Commit 22cc9b2f authored by jiangyz's avatar jiangyz

init

parent ec17f1e5
target/
AGENTS.md
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
<?xml version="1.0" encoding="UTF-8"?>
<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.4.8</version>
<relativePath/>
</parent>
<groupId>com.infoepoch.pms</groupId>
<artifactId>pms-agent</artifactId>
<version>0.0.1</version>
<name>pms-agent</name>
<description>pms-agent</description>
<properties>
<java.version>21</java.version>
<spring-ai.version>1.1.0</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0.4</version>
</dependency>
<dependency>
<groupId>com.oceanbase</groupId>
<artifactId>oceanbase-client</artifactId>
<version>2.2.11</version>
<scope>system</scope>
<systemPath>${pom.basedir}/src/main/resources/jar/oceanbase-client-2.2.11.jar</systemPath>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<version>3.1.4</version>
<executions>
<execution>
<id>install-external</id>
<phase>clean</phase>
<configuration>
<file>${pom.basedir}/src/main/resources/jar/oceanbase-client-2.2.11.jar</file>
<groupId>com.oceanbase</groupId>
<artifactId>oceanbase-client</artifactId>
<version>2.2.11</version>
<packaging>jar</packaging>
<generatePom>true</generatePom>
</configuration>
<goals>
<goal>install-file</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
---
name: spring-ai-alibaba-react-agent
description: Build or update Java and Spring Boot agents with Spring AI Alibaba ReactAgent, including DashScope model setup, ToolCallback definitions, ToolContext usage, structured output, memory, hooks, and invoke-based state access. Use when requests mention Spring AI Alibaba, ReactAgent, DashScope, MemorySaver, RunnableConfig, outputType, outputSchema, or implementing an agent on the Spring stack.
---
# Spring AI Alibaba ReactAgent
Use this skill to implement or refactor Spring AI Alibaba agent code around `ReactAgent`. Keep the skill focused on the framework patterns from the Spring AI Alibaba quick-start, and read [references/quick-start.md](./references/quick-start.md) when exact dependency, API, or assembly details matter.
## Workflow
1. Confirm prerequisites before coding.
JDK 17+ and Maven 3.8+ are the documented baseline. Default to environment variables for API keys, especially `AI_DASHSCOPE_API_KEY`.
2. Pick the model starter before writing agent code.
The quick-start uses `spring-ai-alibaba-agent-framework` plus a model starter such as DashScope. Keep framework and model starter versions aligned to the same release family instead of mixing arbitrary versions.
3. Build the `ChatModel` first.
For DashScope, initialize `DashScopeApi`, then create `DashScopeChatModel`. If you configure `DashScopeChatOptions`, set the model name explicitly before adding temperature and token settings.
4. Define tools with strong metadata.
Prefer `FunctionToolCallback` and clear tool names, descriptions, and parameter descriptions. If the tool needs runtime metadata, implement `BiFunction<Input, ToolContext, Output>` and read values from the `RunnableConfig` stored in the context.
5. Assemble `ReactAgent` from small, explicit pieces.
Start with `.name(...)`, `.model(...)`, `.systemPrompt(...)`, `.tools(...)`, and `.saver(...)`. Add `.outputType(...)`, `.outputSchema(...)`, or `.hooks(...)` only when the behavior requires them.
6. Add memory deliberately.
Use `MemorySaver` for local or example flows. Reuse the same `threadId` in `RunnableConfig` to continue a conversation. For production, swap in a persistent checkpointer instead of in-memory state.
7. Choose response shaping explicitly.
Use `outputType` when you want a Java class to define the response contract. Use `outputSchema` when the output must follow a custom JSON schema-like text contract.
8. Reach for advanced hooks only when there is a concrete need.
Use `invoke(...)` when the caller needs the full state instead of only the last assistant message. Use `ModelCallLimitHook` to cap loops, and use human-in-the-loop hooks when tool execution needs approval.
## Implementation Defaults
- Keep API keys out of source code. Prefer environment variables and property placeholders.
- Keep tool interfaces boring and explicit. Tool names and descriptions become part of the model prompt, so avoid vague naming.
- Prefer a focused system prompt over a long one. State role, available tools, and decision rules.
- Pass request-scoped metadata through `RunnableConfig.addMetadata(...)` when tools need user or tenant context.
- Reuse `threadId` for multi-turn chat. Change `threadId` to start a fresh conversation.
- Treat the versions in the quick-start as documentation examples, not a license to mix incompatible artifacts. Verify the chosen release line before changing a real project.
## Minimal Assembly Pattern
```java
DashScopeApi api = DashScopeApi.builder()
.apiKey(System.getenv("AI_DASHSCOPE_API_KEY"))
.build();
ChatModel model = DashScopeChatModel.builder()
.dashScopeApi(api)
.defaultOptions(DashScopeChatOptions.builder()
.withModel(DashScopeChatModel.DEFAULT_MODEL_NAME)
.withTemperature(0.5)
.withMaxToken(1000)
.build())
.build();
ToolCallback weatherTool = FunctionToolCallback.builder("getWeatherForLocation", new WeatherTool())
.description("Get weather for a given city")
.inputType(String.class)
.build();
ReactAgent agent = ReactAgent.builder()
.name("weather_agent")
.model(model)
.systemPrompt(SYSTEM_PROMPT)
.tools(weatherTool)
.saver(new MemorySaver())
.build();
```
## Decision Rules
- If the user asks for deterministic JSON, prefer `outputType` first and use `outputSchema` only when a custom shape is easier than a Java type.
- If a tool needs request metadata, add it to `RunnableConfig` and read it via `ToolContext`; do not hide it in globals.
- If the user wants follow-up questions to retain context, keep the same `threadId`.
- If the user only needs the assistant text, call `agent.call(...)`; if they need message history or graph state, use `agent.invoke(...)`.
- If an agent may loop on tool or model calls, add `ModelCallLimitHook`.
## References
- Read [references/quick-start.md](./references/quick-start.md) for the extracted quick-start guidance, dependency examples, and advanced features summary.
\ No newline at end of file
interface:
display_name: "Spring AI Alibaba ReactAgent"
short_description: "Build Spring AI Alibaba ReactAgents"
default_prompt: "Use $spring-ai-alibaba-react-agent to build or update a Spring AI Alibaba ReactAgent in this Java project."
\ No newline at end of file
# Spring AI Alibaba Quick Start Reference
Source: https://java2ai.com/docs/quick-start/
Observed page title: `快速开始 | Spring AI Alibaba`
Observed page update line: `最后由 Ken Liu 于 2026年2月2日 更新`
## Scope
This reference captures the implementation patterns shown in the Spring AI Alibaba quick-start for building a `ReactAgent`.
## Documented Prerequisites
- JDK 17+
- Maven 3.8+
- A supported model provider API key
- DashScope is the primary example provider in the page
## Dependency Examples From the Page
The page shows these artifacts:
- `com.alibaba.cloud.ai:spring-ai-alibaba-agent-framework:1.1.2.0`
- `com.alibaba.cloud.ai:spring-ai-alibaba-starter-dashscope:1.1.2.0`
- Later example: `com.alibaba.cloud.ai:spring-ai-alibaba-starter-dashscope:1.1.2.1`
- Later example: `org.springframework.ai:spring-ai-starter-model-openai:1.1.2`
Use these as documentation snapshots. Keep the chosen project on one compatible release line instead of mixing versions casually.
## Configuration Pattern
- Prefer environment variable `AI_DASHSCOPE_API_KEY`
- If using configuration files, inject from the environment:
```yaml
spring:
ai:
dashscope:
api-key: ${AI_DASHSCOPE_API_KEY}
```
## Basic Agent Pattern
The simplest example in the page follows this sequence:
1. Build `DashScopeApi` from the API key
2. Build `DashScopeChatModel`
3. Define a tool with `FunctionToolCallback`
4. Build `ReactAgent`
5. Call `agent.call(...)`
## Tool Design Pattern
The page uses `BiFunction<String, ToolContext, String>` for tools and highlights two cases:
- A pure tool that only needs the user input
- A context-aware tool that reads `RunnableConfig` metadata through `ToolContext`
Important guidance taken from the page:
- Tool name, description, and parameter metadata all influence model behavior
- `@ToolParam` improves parameter descriptions
- `ToolContext` can inject runtime context into tool execution
## Production-Style Agent Assembly
The fuller example assembles a weather agent with:
- A detailed `SYSTEM_PROMPT`
- Two tools: one for weather lookup and one for user location lookup
- `DashScopeChatOptions` for temperature and token limits
- `outputType(ResponseFormat.class)` for structured output
- `MemorySaver` for conversation memory
- `RunnableConfig.builder().threadId(...).addMetadata("user_id", "1")`
## Memory Guidance
- Reuse the same `threadId` to continue a conversation
- The page uses `MemorySaver` for examples
- For production, the page recommends a persistent checkpoint implementation rather than in-memory storage
## Advanced Features Called Out By the Page
### `outputSchema`
Use this when a custom JSON response contract is easier to express as schema text than as a Java class.
### `invoke(...)`
Use this when the caller needs the full `OverAllState`, including message history and other state values.
### `ModelCallLimitHook`
Use this to set a maximum number of model calls and fail fast or exit in a controlled way if the limit is exceeded.
### Human-in-the-loop Hooks
Use hook-based approval when a tool invocation should require confirmation before execution.
## Recommended Working Heuristics
- Start from the smallest working `ReactAgent`, then add memory, output shaping, and hooks one by one
- Keep tool signatures simple and descriptions explicit
- Pass request metadata through `RunnableConfig` instead of hidden static state
- Verify version compatibility before copying dependency coordinates into a real project
\ No newline at end of file
package com.infoepoch.pms.agent;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@ConfigurationPropertiesScan
@ServletComponentScan
@EnableScheduling
@EnableCaching
@EnableAsync
public class PmsAgentApplication {
public static void main(String[] args) {
SpringApplication.run(PmsAgentApplication.class, args);
}
}
\ No newline at end of file
package com.infoepoch.pms.agent.config;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
//@Configuration
public class AiDemoConfiguration {
// @Bean
// OpenAiApi pmsOpenAiApi(PmsAgentProperties properties) {
// return OpenAiApi.builder()
// .baseUrl(properties.getBaseUrl())
// .apiKey(resolveApiKey(properties))
// .build();
// }
//
// @Bean
// ChatModel pmsChatModel(OpenAiApi pmsOpenAiApi, PmsAgentProperties properties) {
// return OpenAiChatModel.builder()
// .openAiApi(pmsOpenAiApi)
// .defaultOptions(OpenAiChatOptions.builder()
// .model(properties.getModel())
// .temperature(properties.getTemperature())
// .maxTokens(properties.getMaxTokens())
// .build())
// .build();
// }
//
// private String resolveApiKey(PmsAgentProperties properties) {
// return StringUtils.hasText(properties.getApiKey()) ? properties.getApiKey() : "EMPTY";
// }
}
\ No newline at end of file
package com.infoepoch.pms.agent.config;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* http json 配置
*/
@Configuration
public class HttpMessageConfig {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* Jackson全局转化long类型为String,解决jackson序列化时long类型缺失精度问题
*
* @return Jackson2ObjectMapperBuilderCustomizer 注入的对象
*/
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder
.indentOutput(true)
.simpleDateFormat("yyyy-MM-dd HH:mm:ss")
.serializerByType(Long.class, ToStringSerializer.instance)
.serializerByType(Long.TYPE, ToStringSerializer.instance)
.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(formatter))
.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(formatter))
.serializationInclusion(JsonInclude.Include.ALWAYS)
;
}
}
package com.infoepoch.pms.agent.config;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Json工具类
*
* @author 余浩
* @date 2019-07-02
* @vision 1.0
*/
public class JsonUtils {
private static final Logger logger = LoggerFactory.getLogger(JsonUtils.class);
// 定义 ObjectMappe 对象
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 私有构造器,禁止外部实例化。
*/
private JsonUtils() {
}
/**
* java 对象转换为 json 字符串
*
* @param objectData java 对象
* @return String json 字符串
*/
public static String objectToJson(Object objectData) {
try {
String string = MAPPER.writeValueAsString(objectData);
return string;
} catch (JsonProcessingException e) {
logger.info("java 对象转换为 json 字符串 出错:{}", e);
e.printStackTrace();
}
return null;
}
/**
* json 字符串转换为 java 对象
*
* @param jsonData json 字符串
* @param beanType java 对象类型
* @return T
*/
public static <T> T jsonToObject(String jsonData, Class<T> beanType) {
// 配置忽略 json 字符串中多余的字段
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
try {
T t = MAPPER.readValue(jsonData, beanType);
return t;
} catch (Exception e) {
e.printStackTrace();
logger.info("json 字符串转换为 java 对象 出错:", e);
throw new RuntimeException("JSON字符转换失败!");
}
}
public static <T> T jsonToObject(String jsonData, TypeReference<T> typeReference) {
// 配置忽略 json 字符串中多余的字段
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
try {
T t = MAPPER.readValue(jsonData, typeReference);
return t;
} catch (Exception e) {
e.printStackTrace();
logger.info("json 字符串转换为 java 对象 出错:", e);
throw new RuntimeException("JSON字符转换失败!");
}
}
/**
* jsonNode 转换为 java 对象
*
* @param jsonNode jsonNode对象
* @param beanType java 对象类型
* @param <T>
* @return
*/
public static <T> T jsonNodeToObject(JsonNode jsonNode, Class<T> beanType) {
String jsonData = objectToJson(jsonNode);
return jsonToObject(jsonData, beanType);
}
/**
* java 对象转换为 JsonNode
*
* @param data java 对象
* @return JsonNode
*/
public static JsonNode objectToJsonNode(Object data) {
JsonNode jsonNode = MAPPER.valueToTree(data);
return jsonNode;
}
/**
* json 字符串转换为 JsonNode
*
* @param jsonData json 字符串
* @return JsonNode
*/
public static JsonNode jsonToJsonNode(String jsonData) {
try {
JsonNode jsonNode = MAPPER.readTree(jsonData);
return jsonNode;
} catch (IOException e) {
logger.info("json 字符串转换为 JsonNode 出错:{}", e);
}
return null;
}
/**
* json 字符串转换为包含 java 对象的 List
*
* @param jsonData json 字符串
* @param beanType java 对象类型
* @return List<T>
*/
public static <T> List<T> jsonToList(String jsonData, Class<T> beanType) {
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType);
try {
List<T> list = MAPPER.readValue(jsonData, javaType);
return list;
} catch (Exception e) {
logger.info("json 字符串转换为包含 java 对象的 List 出错:{}", e);
e.printStackTrace();
}
return null;
}
/**
* JsonNode 对象转换为包含 java 对象的 List
*
* @param jsonNode JsonNode 对象
* @param beanType java 对象类型
* @return List<T>
*/
public static <T> List<T> jsonNodeToList(JsonNode jsonNode, Class<T> beanType) {
String jsonData = objectToJson(jsonNode);
return jsonToList(jsonData, beanType);
}
// region 包含头信息json序列化
private final static String JSON_HEAD = "version";
private final static String JSON_HEAD_MESSAGE = "v1";
private final static String JSON_BODY = "item";
/**
* 将json格式化
*/
public static String formatToJson(String jsonStr) {
return "{\"" + JSON_HEAD + "\":\"" + JSON_HEAD_MESSAGE + "\",\"" + JSON_BODY + "\":" + (io.micrometer.common.util.StringUtils.isNotBlank(jsonStr) ? jsonStr : "[]") + "}";
}
/**
* 将集合转为json
*
* @param <T> 对象类型
*/
public static <T> String toJson(List<T> items) {
ObjectNode objectNode = new ObjectNode(JsonNodeFactory.instance);
objectNode.put(JSON_HEAD, JSON_HEAD_MESSAGE);
objectNode.set(JSON_BODY, new ObjectMapper().valueToTree(items));
return JsonUtils.objectToJson(objectNode);
}
/**
* 将json转为行集合
*
* @param <T> 对象类型
*/
public static <T> List<T> parseJson(String jsonStr, Class<T> tClass) {
JsonNode jsonNode = jsonToJsonNode(jsonStr);
if (jsonNode == null || jsonNode.get(JSON_HEAD) == null)
return null;
String version = jsonNode.get(JSON_HEAD).asText();
return JSON_HEAD_MESSAGE.equalsIgnoreCase(version) ? jsonNodeToList(jsonNode.get(JSON_BODY), tClass) : null;
}
// endregion
public static String filterToString(SimpleFilterProvider simpleFilterProvider, Object object) {
MAPPER.setFilterProvider(simpleFilterProvider);
try {
return MAPPER.writeValueAsString(object);
} catch (JsonProcessingException e) {
logger.info("序列化过滤错误!\n{}", printToJsonString(object));
e.printStackTrace();
}
return "";
}
/**
* 美化JSON输出
*/
public static String printToJsonString(Object object) {
try {
return "\n" + MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(object);
} catch (JsonProcessingException e) {
logger.info("JSON打印异常。");
e.printStackTrace();
}
return "";
}
public static Map<String, Object> jsonToMap(String jsonStr) {
Map<String, Object> tmpMap = new HashMap<>();
try {
tmpMap = MAPPER.readValue(jsonStr, Map.class);
} catch (Exception ex) {
logger.info("JSON转换异常。");
ex.printStackTrace();
}
return tmpMap;
}
public static List<Map<String, Object>> jsonToMapList(String jsonStr) {
List<Map<String, Object>> mapList = new ArrayList<>();
try {
mapList = MAPPER.readValue(jsonStr, new TypeReference<List<Map<String, Object>>>() {
@Override
public Type getType() {
return super.getType();
}
});
} catch (Exception ex) {
logger.info("JSON转换异常。");
ex.printStackTrace();
}
return mapList;
}
}
package com.infoepoch.pms.agent.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Getter
@Setter
@ConfigurationProperties(prefix = "pms.agent")
public class PmsAgentProperties {
private String baseUrl;
private String apiKey;
private String model;
private double temperature;
private int maxTokens;
private int maxIterations;
private String systemPrompt;
}
\ No newline at end of file
package com.infoepoch.pms.agent.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfiguration {
@Bean
RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
StringRedisSerializer keySerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer valueSerializer =
new GenericJackson2JsonRedisSerializer(redisObjectMapper());
redisTemplate.setKeySerializer(keySerializer);
redisTemplate.setHashKeySerializer(keySerializer);
redisTemplate.setValueSerializer(valueSerializer);
redisTemplate.setHashValueSerializer(valueSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
private ObjectMapper redisObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return objectMapper;
}
}
\ No newline at end of file
package com.infoepoch.pms.agent.service;
import com.infoepoch.pms.agent.config.JsonUtils;
import com.infoepoch.pms.agent.config.PmsAgentProperties;
import com.infoepoch.pms.agent.web.dto.AgentChatRequest;
import com.infoepoch.pms.agent.web.dto.AgentChatResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class PmsAgentService {
private static final long SSE_TIMEOUT = 0L;
// private final ChatModel pmsChatModel;
private final PmsAgentProperties properties;
public void chatTest() {
OpenAiApi openAiApi = OpenAiApi.builder().baseUrl("https://api.siliconflow.cn").apiKey(
"sk-necxquynovkphnulbbfbsopeosgogrsuqgxxctzhbcbpntyh").build();
ChatModel chatModel = OpenAiChatModel.builder().openAiApi(openAiApi).build();
Prompt prompt = new Prompt("你好!",
OpenAiChatOptions.builder()
.model("Qwen/Qwen3.5-27B")
.temperature(0.7)
.build());
final ChatResponse chatResponse = chatModel.call(prompt);
System.out.println(JsonUtils.objectToJson(chatResponse));
}
public void chatTest1() {
OpenAiApi openAiApi = OpenAiApi.builder().baseUrl("http://172.28.30.253:8001").apiKey(
"115e68c85d91d40dd3df42a06f08cbed").build();
ChatModel chatModel = OpenAiChatModel.builder().openAiApi(openAiApi).build();
Prompt prompt = new Prompt("你好!",
OpenAiChatOptions.builder()
.model("Qwen3.5-27B")
.temperature(0.7)
.build());
final ChatResponse chatResponse = chatModel.call(prompt);
System.out.println(JsonUtils.objectToJson(chatResponse));
}
// public AgentChatResponse chat(AgentChatRequest request) {
// ChatResponse response = pmsChatModel.call(buildPrompt(request.message()));
// return new AgentChatResponse(extractText(response));
// }
public SseEmitter streamChat(AgentChatRequest request) {
SseEmitter emitter = new SseEmitter(SSE_TIMEOUT);
Prompt prompt = buildPrompt("你好");
// ChatResponse s = pmsChatModel.call(prompt);
// System.out.println(s);
// pmsChatModel.stream(buildPrompt(request.message()))
// .map(this::extractText)
// .filter(StringUtils::hasText)
// .doOnNext(token -> sendToken(emitter, token))
// .doOnComplete(() -> completeStream(emitter))
// .doOnError(emitter::completeWithError)
// .subscribe();
return emitter;
}
private Prompt buildPrompt(String message) {
List<Message> messages = new ArrayList<>();
if (StringUtils.hasText(properties.getSystemPrompt())) {
messages.add(new SystemMessage(properties.getSystemPrompt()));
}
messages.add(new UserMessage(message));
OpenAiChatOptions options = OpenAiChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.build();
return new Prompt(messages, options);
}
private String extractText(ChatResponse response) {
if (response == null || response.getResult() == null || response.getResult().getOutput() == null) {
return "";
}
return response.getResult().getOutput().getText();
}
private void sendToken(SseEmitter emitter, String token) {
try {
emitter.send(SseEmitter.event().name("token").data(token));
} catch (Exception exception) {
emitter.completeWithError(exception);
}
}
private void completeStream(SseEmitter emitter) {
try {
emitter.send(SseEmitter.event().name("done").data("[DONE]"));
emitter.complete();
} catch (Exception exception) {
emitter.completeWithError(exception);
}
}
}
\ No newline at end of file
package com.infoepoch.pms.agent.tool;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.time.DateTimeException;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.function.BiFunction;
@Component
public class CurrentTimeTool implements BiFunction<String, ToolContext, String> {
@Override
public String apply(@ToolParam(description = "Optional timezone such as Asia/Shanghai") String timezone,
ToolContext toolContext) {
ZoneId zoneId = resolveZoneId(timezone);
return ZonedDateTime.now(zoneId).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
}
private ZoneId resolveZoneId(String timezone) {
if (!StringUtils.hasText(timezone)) {
return ZoneId.systemDefault();
}
try {
return ZoneId.of(timezone.trim());
} catch (DateTimeException exception) {
return ZoneId.systemDefault();
}
}
}
package com.infoepoch.pms.agent.web;
import com.infoepoch.pms.agent.service.PmsAgentService;
import com.infoepoch.pms.agent.web.dto.AgentChatRequest;
import com.infoepoch.pms.agent.web.dto.AgentChatResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
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.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/agent")
public class AgentChatController {
private final PmsAgentService pmsAgentService;
// @PostMapping("/chat")
// public ResponseEntity<AgentChatResponse> chat(@Valid @RequestBody AgentChatRequest request) {
// return ResponseEntity.ok(pmsAgentService.chat(request));
// }
//
// @PostMapping(path = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
// public SseEmitter streamChat(@Valid @RequestBody AgentChatRequest request) {
// return pmsAgentService.streamChat(request);
// }
@PostMapping("/chatTest")
public void chatTest() {
pmsAgentService.chatTest();
}
@PostMapping("/chatTest2")
public void chatTest2() {
pmsAgentService.chatTest1();
}
@PostMapping("/testChat3")
public void testChat3() throws URISyntaxException {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
String url = "http://172.28.30.253:8001/v1/chat/completions";
try {
URI uri = new URI(url);
ClientHttpRequest request = factory.createRequest(uri, HttpMethod.POST);
// 4. 设置请求头
request.getHeaders().set("Content-Type", "application/json");
request.getHeaders().set("Accept", "application/json");
request.getHeaders().set("Authorization", "Bearer 115e68c85d91d40dd3df42a06f08cbed");
// 5. 准备请求体(例如 JSON 字符串)
String requestBody = "{\n" +
" \"model\": \"Qwen3.5-27B\",\n" +
" \"messages\": [\n" +
" {\"role\": \"user\", \"content\": \"你好\"}\n" +
" ]\n" +
"}";
// 6. 写入请求体
try (OutputStream os = request.getBody()) {
os.write(requestBody.getBytes(StandardCharsets.UTF_8));
os.flush();
}
// 7. 执行请求并获取响应
try (ClientHttpResponse response = request.execute()) {
// 8. 处理响应
HttpStatusCode statusCode = response.getStatusCode();
System.out.println("Status Code: " + statusCode);
// 读取响应体
String responseBody = new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8);
System.out.println("Response Body: " + responseBody);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
\ No newline at end of file
package com.infoepoch.pms.agent.web.dto;
import jakarta.validation.constraints.NotBlank;
public record AgentChatRequest(
@NotBlank(message = "message must not be blank")
String message
) {
}
\ No newline at end of file
package com.infoepoch.pms.agent.web.dto;
public record AgentChatResponse(
String content
) {
}
\ No newline at end of file
environment-ip: ENC(qMGm7BJ9sQkx2FWU9JJuHIl1b7pwY3NO)
#environment-ip: localhost
server:
port: 8406
spring:
data:
redis:
host: ENC(qMGm7BJ9sQkx2FWU9JJuHIl1b7pwY3NO)
port: 6379
password: ENC(dNAOQsOxmsI1L/HsxZ84c6Lw7nQs9ywa)
# cluster:
# nodes:
# - 172.28.30.62:6379
# - 172.28.30.62:6380
# - 172.28.30.62:6381
# - 172.28.30.62:6382
# - 172.28.30.62:6383
# - 172.28.30.62:6384
jedis:
pool:
# 连接池的最大数据库连接数
max-active: 9
#连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: 2000
# 连接池中的最大空闲连接
max-idle: 9
# 连接池中的最小空闲连接
min-idle: 0
#每ms运行一次空闲连接回收器(独立线程)
time-between-eviction-runs: 1000
datasource:
driver-class-name: oracle.jdbc.OracleDriver
url: jdbc:oracle:thin:@172.28.30.71:1521:DEVEPMS
username: dbunionjs
password: ENC(0SNhMqnAZf/8CCmyXOIjcA==)
\ No newline at end of file
environment-ip: xxharmapp.js.cmcc
server:
port: 8406
spring:
data:
redis:
# host: xxharmapp.js.cmcc
# port: 30305
# password: Epoinfo0004|
password: ENC(TSg7xEDgEKT+Vtsq6yrM34fRe+rk0vrL)
cluster:
nodes:
- 2409:8020:5c05:200::307:3191@39025
- 2409:8020:5c05:200::307:3192@39025
- 2409:8020:5c05:200::307:3193@39025
- 2409:8020:5c05:200::307:3191@33245
- 2409:8020:5c05:200::307:3192@33245
- 2409:8020:5c05:200::307:3193@33245
# - 10.32.49.145:39025
# - 10.32.49.146:39025
# - 10.32.49.147:39025
# - 10.32.49.145:33245
# - 10.32.49.146:33245
# - 10.32.49.147:33245
jedis:
pool:
max-active: 8
max-wait: 2000
max-idle: 8
min-idle: 0
time-between-eviction-runs: 1000
datasource:
driver-class-name: com.alipay.oceanbase.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:oceanbase://10.32.166.11:2883,10.32.166.12:2883,10.32.166.13:2883,10.32.166.14:2883,10.32.166.15:2883,10.32.166.16:2883/JSXJY12?continueBatchOnError=false&allowMultiQueries=true&rewriteBatchedStatements=true&loadBalanceStrategy=RANDOM
#格式: 用户名@租户名#集群名
username: JSXJY12@epmsdb#OAFCDB1
password: ENC(Yj7c5E7DUeoC2U3nXdqxpbm97z40p662)
\ No newline at end of file
environment-ip: ENC(w1+POo9hLUzUYyV7oTAjXnPioWYssX77)
server:
port: 8406
spring:
data:
redis:
host: ENC(w1+POo9hLUzUYyV7oTAjXnPioWYssX77)
port: 30325
# password: Epoinfo0004|
jedis:
pool:
# 连接池的最大数据库连接数
max-active: 8
#连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: 2000
# 连接池中的最大空闲连接
max-idle: 8
# 连接池中的最小空闲连接
min-idle: 0
#每ms运行一次空闲连接回收器(独立线程)
time-between-eviction-runs: 1000
datasource:
driver-class-name: com.alipay.oceanbase.jdbc.Driver
# DB数据库信息从paas平台里面动态配置 jdbc:oceanbase://10.33.221.190:2883
url: jdbc:oceanbase://10.33.240.207:2881?rewriteBatchedStatements=TRUE
#格式: 用户名@租户名#集群名
username: unionjs@gxxjycsdb
password: ENC(0SNhMqnAZf/8CCmyXOIjcA==)
\ No newline at end of file
spring:
profiles:
active: dev
servlet:
multipart:
enabled: true
max-file-size: 200MB
max-request-size: 200MB
jdbc:
template:
fetch-size: 500
application:
name: pms-agent
ai:
openai:
api-key: 115e68c85d91d40dd3df42a06f08cbed
server:
servlet:
context-path: /${spring.application.name}
session:
timeout: 7200
tomcat:
max-http-form-post-size: 50MB
logging:
file:
name: /data/${spring.application.name}/log/${spring.application.name}.log
level:
com.infoepoch.pms.agent: info
pms:
agent:
base-url: http://172.28.30.253:8001
api-key: 115e68c85d91d40dd3df42a06f08cbed
model: Qwen3.5-27B
temperature: 0.5
max-tokens: 1000
max-iterations: 5
system-prompt: 你是一个简洁的中文助手,请直接回答用户问题。
jasypt:
encryptor:
algorithm: PBEWithMD5AndDES
iv-generator-classname: org.jasypt.iv.NoIvGenerator
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment