Commit da58786b authored by jiangyz's avatar jiangyz

精准关爱智能体

parent e8f83315
...@@ -15,15 +15,28 @@ ...@@ -15,15 +15,28 @@
<description>pms-agent</description> <description>pms-agent</description>
<properties> <properties>
<java.version>21</java.version> <java.version>17</java.version>
<spring-ai.version>1.1.2</spring-ai.version>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-bom</artifactId>
<version>1.1.2.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId> <artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version> <version>1.1.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-extensions-bom</artifactId>
<version>1.1.2.1</version>
<type>pom</type> <type>pom</type>
<scope>import</scope> <scope>import</scope>
</dependency> </dependency>
...@@ -56,10 +69,6 @@ ...@@ -56,10 +69,6 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId> <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
...@@ -119,6 +128,19 @@ ...@@ -119,6 +128,19 @@
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-agent-framework</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>
......
--- ---
name: spring-ai-alibaba-react-agent 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. description: Build or update Java and Spring Boot agents with Spring AI Alibaba ReactAgent using the official Agents tutorial patterns, including ChatModel setup, ToolCallback tools, systemPrompt or instruction design, call or invoke usage, RunnableConfig threadId and metadata, outputType or outputSchema, MemorySaver, hooks, interceptors, and streaming-oriented agent integration. Use when requests mention Spring AI Alibaba, ReactAgent, RunnableConfig, ToolContext, MemorySaver, outputType, outputSchema, hooks, interceptors, or implementing agent workflows on the Spring stack.
--- ---
# Spring AI Alibaba ReactAgent # 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. Use this skill to implement or refactor Spring AI Alibaba agent code around `ReactAgent`. Keep the implementation aligned with the official Agents tutorial, and read [references/agents.md](./references/agents.md) when exact API shape, invocation mode, or advanced capability details matter.
## Workflow ## Workflow
1. Confirm prerequisites before coding. 1. Build the smallest agent that solves the request.
JDK 17+ and Maven 3.8+ are the documented baseline. Default to environment variables for API keys, especially `AI_DASHSCOPE_API_KEY`. Start from `ChatModel + ReactAgent + call(...)`. Add tools, memory, state access, hooks, or streaming only when the request needs them.
2. Pick the model starter before writing agent code. 2. Configure the model first.
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. Create the provider `ChatModel` explicitly. If the project already has a model bean, reuse it instead of introducing a second configuration style.
3. Build the `ChatModel` first. 3. Add tools only when the agent must act.
For DashScope, initialize `DashScopeApi`, then create `DashScopeChatModel`. If you configure `DashScopeChatOptions`, set the model name explicitly before adding temperature and token settings. Use `FunctionToolCallback` with explicit tool names and short descriptions. If the tool needs request-scoped metadata, read it through `ToolContext` from `RunnableConfig`.
4. Define tools with strong metadata. 4. Choose prompt style deliberately.
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. Use `systemPrompt(...)` for a compact role definition. Use `instruction(...)` for a longer task-specific directive.
5. Assemble `ReactAgent` from small, explicit pieces. 5. Pick the invocation mode based on the caller contract.
Start with `.name(...)`, `.model(...)`, `.systemPrompt(...)`, `.tools(...)`, and `.saver(...)`. Add `.outputType(...)`, `.outputSchema(...)`, or `.hooks(...)` only when the behavior requires them. Use `call(...)` for the final answer. Use `invoke(...)` when the caller needs `OverAllState`, message history, or custom state.
6. Add memory deliberately. 6. Add conversation 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. Use `MemorySaver` and reuse the same `threadId` in `RunnableConfig` for multi-turn chat. For production, prefer a persistent saver.
7. Choose response shaping explicitly. 7. Shape outputs only when the consumer requires it.
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. Prefer `outputType(...)` for stable Java contracts. Use `outputSchema(...)` when a schema-like response contract is more practical.
8. Reach for advanced hooks only when there is a concrete need. 8. Add hooks, interceptors, or streaming only after the basic path works.
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. Use `ModelCallLimitHook` to cap loops, interceptors for tool or prompt customization, and wrap streaming output in your own application contract.
## Implementation Defaults ## Implementation Defaults
- Keep API keys out of source code. Prefer environment variables and property placeholders. - Keep API keys out of source code. Prefer environment variables and Spring properties.
- Keep tool interfaces boring and explicit. Tool names and descriptions become part of the model prompt, so avoid vague naming. - Keep tools narrow and explicit. Tool names and descriptions directly affect model behavior.
- Prefer a focused system prompt over a long one. State role, available tools, and decision rules. - Prefer the shortest prompt that enforces the needed behavior.
- Pass request-scoped metadata through `RunnableConfig.addMetadata(...)` when tools need user or tenant context. - Pass request metadata through `RunnableConfig.addMetadata(...)` instead of globals.
- Reuse `threadId` for multi-turn chat. Change `threadId` to start a fresh conversation. - Reuse `threadId` for multi-turn continuity. Change it for a fresh session.
- 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. - Keep the controller thin and push agent assembly or orchestration into the service layer.
- Do not expose raw low-level graph streaming objects directly to external callers unless the project already standardizes on them.
## Minimal Assembly Pattern ## Minimal Assembly Pattern
```java ```java
DashScopeApi api = DashScopeApi.builder() DashScopeApi dashScopeApi = DashScopeApi.builder()
.apiKey(System.getenv("AI_DASHSCOPE_API_KEY")) .apiKey(System.getenv("AI_DASHSCOPE_API_KEY"))
.build(); .build();
ChatModel model = DashScopeChatModel.builder() ChatModel chatModel = DashScopeChatModel.builder()
.dashScopeApi(api) .dashScopeApi(dashScopeApi)
.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(); .build();
ReactAgent agent = ReactAgent.builder() ReactAgent agent = ReactAgent.builder()
.name("weather_agent") .name("my_agent")
.model(model) .model(chatModel)
.systemPrompt(SYSTEM_PROMPT)
.tools(weatherTool)
.saver(new MemorySaver())
.build(); .build();
AssistantMessage response = agent.call("杭州的天气怎么样?");
System.out.println(response.getText());
``` ```
## Decision Rules ## 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 the caller only needs the final assistant text, use `call(...)`.
- If a tool needs request metadata, add it to `RunnableConfig` and read it via `ToolContext`; do not hide it in globals. - If the caller needs state, messages, or custom graph values, use `invoke(...)`.
- If the user wants follow-up questions to retain context, keep the same `threadId`. - If follow-up requests must retain context, add `.saver(...)` and reuse `threadId`.
- If the user only needs the assistant text, call `agent.call(...)`; if they need message history or graph state, use `agent.invoke(...)`. - If the agent must use runtime business context, pass it via `RunnableConfig` and read it through `ToolContext`.
- If an agent may loop on tool or model calls, add `ModelCallLimitHook`. - If the output must map to a stable Java contract, prefer `outputType(...)`.
- If the agent may over-iterate, add `ModelCallLimitHook`.
- If the request is for HTTP streaming, keep the agent logic in the service layer and convert framework output to a stable SSE or chunked response shape.
## References ## References
- Read [references/quick-start.md](./references/quick-start.md) for the extracted quick-start guidance, dependency examples, and advanced features summary. - Read [references/agents.md](./references/agents.md) for the extracted official tutorial guidance on agent assembly, invocation, memory, hooks, interceptors, and streaming-related decisions.
\ No newline at end of file
interface: interface:
display_name: "Spring AI Alibaba ReactAgent" display_name: "Spring AI Alibaba ReactAgent"
short_description: "Build Spring AI Alibaba ReactAgents" short_description: "Build ReactAgents from official patterns"
default_prompt: "Use $spring-ai-alibaba-react-agent to build or update a Spring AI Alibaba ReactAgent in this Java project." default_prompt: "Use $spring-ai-alibaba-react-agent to build or update a Spring AI Alibaba ReactAgent in this Java project using the official Agents tutorial patterns."
\ 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
...@@ -14,6 +14,9 @@ import org.springframework.http.client.SimpleClientHttpRequestFactory; ...@@ -14,6 +14,9 @@ import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient;
/**
* 办公网模型
*/
@Configuration @Configuration
public class AiLocalConfiguration { public class AiLocalConfiguration {
......
package com.infoepoch.pms.agent.aiMode;
import com.infoepoch.pms.agent.properties.AiLocalProperties;
import com.infoepoch.pms.agent.properties.AiSiliconFlowProperties;
import com.infoepoch.pms.agent.properties.AiZhiYuProperties;
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.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
/**
* @author jiangyz
* @date 2026/4/2 10:38
* 硅基流动模型
*/
@Configuration
public class AiSiliconFlowConfiguration {
@Bean("SiliconFlowOpenAiApi")
OpenAiApi siliconFlowOpenAiApi(AiSiliconFlowProperties properties) {
return OpenAiApi.builder()
.baseUrl(properties.getBaseUrl())
.apiKey(resolveApiKey(properties))
.build();
}
@Bean("SiliconFlowChatModel")
ChatModel SiliconFlowChatModel(@Qualifier("SiliconFlowOpenAiApi") OpenAiApi siliconFlowOpenAiApi,
AiSiliconFlowProperties properties) {
Map<String, Object> extraBody = new HashMap<>();
extraBody.put("enable_thinking", false);//关闭思考
return OpenAiChatModel.builder()
.openAiApi(siliconFlowOpenAiApi)
.defaultOptions(OpenAiChatOptions.builder()
.model(properties.getModel())
.extraBody(extraBody)
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.build())
.build();
}
private String resolveApiKey(AiSiliconFlowProperties properties) {
return StringUtils.hasText(properties.getApiKey()) ? properties.getApiKey() : "EMPTY";
}
}
...@@ -15,6 +15,9 @@ import org.springframework.http.client.SimpleClientHttpRequestFactory; ...@@ -15,6 +15,9 @@ import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient;
/**
* 智宇模型
*/
@Configuration @Configuration
public class AiZhiYuConfiguration { public class AiZhiYuConfiguration {
......
package com.infoepoch.pms.agent.aiMode;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author jiangyz
* @date 2026/4/2 9:58
*/
@Configuration
public class CareAgentConfiguration {
@Bean
ReactAgent CareAgent(@Qualifier("SiliconFlowChatModel") ChatModel chatModel) {
return ReactAgent.builder()
.name("careAgent")
.model(chatModel)
.systemPrompt("""
你是精准关爱智能体。
当用户需要推荐活动时,结合调用方提供的真实用户、活动和偏好信息给出推荐和简短理由。
当用户需要推荐用户时,结合调用方提供的活动语义、筛选条件和真实候选信息给出推荐和简短理由。
当用户进行通用对话时,保持精准关爱智能体身份,自然回复用户。
回答要求:
- 推荐类内容只能依据调用方提供的真实数据作答
- 不能编造不存在的活动、用户、历史记录、标签、数量或事实
- 必须使用中文回答
- 推荐类回答只输出约定模板所需内容,不自由发挥版式
- 保持自然、友好、简洁
- 绝不能提到分页、页码、后台处理中、接口调用、候选集、模型或任何技术实现细节
- 当结果不足时,只输出实际合适的结果,并自然说明
""")
.build();
}
}
package com.infoepoch.pms.agent.common;
import org.slf4j.LoggerFactory;
/**
* Author Anyho(wuh@infoepoh.com)
* Time 2019/12/10 9:35 星期二
*/
public class LogHelper {
public static void debug(Object context, String s) {
LoggerFactory.getLogger(context.getClass()).debug(s);
}
public static void debug(Object context, String s, Object o) {
LoggerFactory.getLogger(context.getClass()).debug(s, o);
}
public static void debug(Object context, String s, Object o, Object o1) {
LoggerFactory.getLogger(context.getClass()).debug(s, o, o1);
}
public static void debug(Object context, String s, Object... objects) {
LoggerFactory.getLogger(context.getClass()).debug(s, objects);
}
public static void debug(Object context, String s, Throwable throwable) {
LoggerFactory.getLogger(context.getClass()).debug(s, throwable);
}
public static void info(Object context, String s) {
LoggerFactory.getLogger(context.getClass()).info(s);
}
public static void info(Object context, String s, Object o) {
LoggerFactory.getLogger(context.getClass()).info(s, o);
}
public static void info(Object context, String s, Object o, Object o1) {
LoggerFactory.getLogger(context.getClass()).info(s, o, o1);
}
public static void info(Object context, String s, Object... objects) {
LoggerFactory.getLogger(context.getClass()).info(s, objects);
}
public static void info(Object context, String s, Throwable throwable) {
LoggerFactory.getLogger(context.getClass()).info(s, throwable);
}
public static void error(Object context, String s) {
LoggerFactory.getLogger(context.getClass()).error(s);
}
public static void error(Object context, String s, Object o) {
LoggerFactory.getLogger(context.getClass()).error(s, o);
}
public static void error(Object context, String s, Object o, Object o1) {
LoggerFactory.getLogger(context.getClass()).error(s, o, o1);
}
public static void error(Object context, String s, Object... objects) {
LoggerFactory.getLogger(context.getClass()).error(s, objects);
}
public static void error(Object context, String s, Throwable throwable) {
LoggerFactory.getLogger(context.getClass()).error(s, throwable);
}
public static void error(Class clazz, String s, Throwable throwable) {
LoggerFactory.getLogger(clazz).error(s, throwable);
}
public static void error(Class clazz, String s, Object... objects) {
LoggerFactory.getLogger(clazz).error(s, objects);
}
public static void error(Class clazz, String s, Object o, Object o1) {
LoggerFactory.getLogger(clazz).error(s, o, o1);
}
}
...@@ -12,7 +12,7 @@ import java.time.LocalDateTime; ...@@ -12,7 +12,7 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
/** /**
* http json 配置 * json配置
*/ */
@Configuration @Configuration
public class HttpMessageConfig { public class HttpMessageConfig {
...@@ -29,6 +29,7 @@ public class HttpMessageConfig { ...@@ -29,6 +29,7 @@ public class HttpMessageConfig {
.indentOutput(true) .indentOutput(true)
.simpleDateFormat("yyyy-MM-dd HH:mm:ss") .simpleDateFormat("yyyy-MM-dd HH:mm:ss")
.serializerByType(Long.class, ToStringSerializer.instance) .serializerByType(Long.class, ToStringSerializer.instance)
.serializerByType(Long.class, ToStringSerializer.instance)
.serializerByType(Long.TYPE, ToStringSerializer.instance) .serializerByType(Long.TYPE, ToStringSerializer.instance)
.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(formatter)) .serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(formatter))
.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(formatter)) .deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(formatter))
......
...@@ -6,9 +6,11 @@ import com.fasterxml.jackson.databind.DeserializationFeature; ...@@ -6,9 +6,11 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
...@@ -34,6 +36,12 @@ public class JsonUtils { ...@@ -34,6 +36,12 @@ public class JsonUtils {
// 定义 ObjectMappe 对象 // 定义 ObjectMappe 对象
private static final ObjectMapper MAPPER = new ObjectMapper(); private static final ObjectMapper MAPPER = new ObjectMapper();
static {
MAPPER.registerModule(new JavaTimeModule());
MAPPER.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
/** /**
* 私有构造器,禁止外部实例化。 * 私有构造器,禁止外部实例化。
*/ */
...@@ -65,8 +73,6 @@ public class JsonUtils { ...@@ -65,8 +73,6 @@ public class JsonUtils {
* @return T * @return T
*/ */
public static <T> T jsonToObject(String jsonData, Class<T> beanType) { public static <T> T jsonToObject(String jsonData, Class<T> beanType) {
// 配置忽略 json 字符串中多余的字段
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
try { try {
T t = MAPPER.readValue(jsonData, beanType); T t = MAPPER.readValue(jsonData, beanType);
return t; return t;
...@@ -78,8 +84,6 @@ public class JsonUtils { ...@@ -78,8 +84,6 @@ public class JsonUtils {
} }
public static <T> T jsonToObject(String jsonData, TypeReference<T> typeReference) { public static <T> T jsonToObject(String jsonData, TypeReference<T> typeReference) {
// 配置忽略 json 字符串中多余的字段
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
try { try {
T t = MAPPER.readValue(jsonData, typeReference); T t = MAPPER.readValue(jsonData, typeReference);
return t; return t;
...@@ -138,7 +142,6 @@ public class JsonUtils { ...@@ -138,7 +142,6 @@ public class JsonUtils {
* @return List<T> * @return List<T>
*/ */
public static <T> List<T> jsonToList(String jsonData, Class<T> beanType) { 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); JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType);
try { try {
List<T> list = MAPPER.readValue(jsonData, javaType); List<T> list = MAPPER.readValue(jsonData, javaType);
......
package com.infoepoch.pms.agent.controller;
import com.infoepoch.pms.agent.common.LogHelper;
import com.infoepoch.pms.agent.domain.care.CareAgentService;
import com.infoepoch.pms.agent.domain.care.log.CareTraceLogSupport;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/agent")
public class AgentChatController {
private final CareAgentService careAgentService;
/**
* 推荐场景的标准 GET 方式的流式聊天入口。
*/
@GetMapping(value = "/care/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamCareAgent(@RequestParam("sessionId") String sessionId,
@RequestParam("message") String message) {
return streamCareAgentInternal(sessionId, message);
}
/**
* 将领域服务输出统一封装成 SSE 事件流。
*/
private Flux<ServerSentEvent<String>> streamCareAgentInternal(String sessionId, String message) {
String traceId = CareTraceLogSupport.newTraceId();
LogHelper.info(this, CareTraceLogSupport.format(
traceId,
sessionId,
"流式请求开始",
"收到请求," + CareTraceLogSupport.safeMessageSummary(message)
));
if (!StringUtils.hasText(sessionId)) {
LogHelper.info(this, CareTraceLogSupport.format(
traceId,
sessionId,
"参数校验失败",
"sessionId为空"
));
return Flux.just(buildEvent("error", "sessionId不能为空"));
}
return careAgentService.streamChat(traceId, sessionId, message)
.map(chunk -> buildEvent("message", chunk))
.concatWithValues(buildEvent("done", "[DONE]"))
.doOnComplete(() -> LogHelper.info(this, CareTraceLogSupport.format(
traceId,
sessionId,
"流式请求完成",
"SSE输出完成"
)))
.doOnError(error -> LogHelper.error(this, CareTraceLogSupport.format(
traceId,
sessionId,
"流式请求异常",
"SSE输出失败,异常=" + CareTraceLogSupport.safeText(resolveErrorMessage(error))
), error))
.onErrorResume(error -> Flux.just(buildEvent("error", resolveErrorMessage(error))));
}
/**
* 构造统一的 SSE 消息事件。
*/
private ServerSentEvent<String> buildEvent(String event, String data) {
return ServerSentEvent.<String>builder()
.event(event)
.data(data)
.build();
}
/**
* 将异常转换成可直接返回给前端的错误文案。
*/
private String resolveErrorMessage(Throwable error) {
return StringUtils.hasText(error.getMessage()) ? error.getMessage() : "careAgent流式调用失败";
}
}
package com.infoepoch.pms.agent.domain.care;
import com.infoepoch.pms.agent.common.LogHelper;
import com.infoepoch.pms.agent.domain.care.log.CareTraceLogSupport;
import com.infoepoch.pms.agent.domain.care.orchestrator.CareRecommendationOrchestrator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
/**
* @author jiangyz
* @date 2026/4/2 9:04
* 精准关爱智能体
*/
@Service
public class CareAgentService {
private final CareRecommendationOrchestrator careRecommendationOrchestrator;
public CareAgentService(CareRecommendationOrchestrator careRecommendationOrchestrator) {
this.careRecommendationOrchestrator = careRecommendationOrchestrator;
}
/**
* 精准关爱智能体统一流式入口,负责基础校验和异常兜底。
*/
public Flux<String> streamChat(String traceId, String sessionId, String msg) {
if (StringUtils.isEmpty(msg)) {
LogHelper.info(this, CareTraceLogSupport.format(
traceId,
sessionId,
"参数校验失败",
"message为空"
));
return Flux.error(new IllegalArgumentException("message不能为空"));
}
LogHelper.info(this, CareTraceLogSupport.format(
traceId,
sessionId,
"服务开始处理",
"进入编排层处理"
));
try {
return careRecommendationOrchestrator.stream(traceId, sessionId, msg)
.doOnComplete(() -> LogHelper.info(this, CareTraceLogSupport.format(
traceId,
sessionId,
"服务处理完成",
"编排层处理完成"
)))
.doOnError(error -> LogHelper.error(this, CareTraceLogSupport.format(
traceId,
sessionId,
"服务处理异常",
"编排层处理失败,异常=" + CareTraceLogSupport.safeText(error.getMessage())
), error));
} catch (Exception e) {
LogHelper.error(this, CareTraceLogSupport.format(
traceId,
sessionId,
"服务处理异常",
"streamChat立即失败,异常=" + CareTraceLogSupport.safeText(e.getMessage())
), e);
return Flux.error(new IllegalArgumentException("调用careAgent失败"));
}
}
}
package com.infoepoch.pms.agent.properties;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author jiangyz
* @date 2026/4/2 10:41
*/
@Getter
@Setter
@ConfigurationProperties(prefix = "pms.ai.siliconflow")
public class AiSiliconFlowProperties {
private String baseUrl;
private String apiKey;
private String model;
private double temperature;
private int maxTokens;
private int maxIterations;
private String systemPrompt;
}
package com.infoepoch.pms.agent.service;
import com.infoepoch.pms.agent.config.JsonUtils;
import com.infoepoch.pms.agent.properties.AiZhiYuProperties;
import com.infoepoch.pms.agent.web.dto.AgentChatRequest;
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.OpenAiChatOptions;
import org.springframework.beans.factory.annotation.Qualifier;
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
public class PmsAgentService {
private final ChatModel pmsChatModel;
public PmsAgentService(@Qualifier("localChatModel") ChatModel pmsChatModel){
this.pmsChatModel = pmsChatModel;
}
public void chatTest() {
ChatResponse chatResponse = pmsChatModel.call(new Prompt("你好"));
System.out.println(chatResponse.getResult().getOutput().getText());
}
}
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 lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@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();
}
}
\ No newline at end of file
package com.infoepoch.pms.agent.web.dto;
public record AgentChatRequest(
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
...@@ -51,3 +51,10 @@ pms: ...@@ -51,3 +51,10 @@ pms:
temperature: 0.5 temperature: 0.5
max-tokens: 50000 max-tokens: 50000
max-iterations: 5 max-iterations: 5
siliconflow:
base-url: https://api.siliconflow.cn
api-key: sk-necxquynovkphnulbbfbsopeosgogrsuqgxxctzhbcbpntyh
model: Qwen/Qwen3.5-122B-A10B
temperature: 0.2
max-tokens: 50000
max-iterations: 5
...@@ -53,3 +53,10 @@ pms: ...@@ -53,3 +53,10 @@ pms:
temperature: 0.5 temperature: 0.5
max-tokens: 50000 max-tokens: 50000
max-iterations: 5 max-iterations: 5
siliconFlow:
base-url: https://api.siliconflow.cn
api-key: sk-necxquynovkphnulbbfbsopeosgogrsuqgxxctzhbcbpntyh
model: Qwen/Qwen3.5-27B
temperature: 0.2
max-tokens: 50000
max-iterations: 5
\ No newline at end of file
...@@ -44,3 +44,10 @@ pms: ...@@ -44,3 +44,10 @@ pms:
temperature: 0.5 temperature: 0.5
max-tokens: 50000 max-tokens: 50000
max-iterations: 5 max-iterations: 5
siliconFlow:
base-url: https://api.siliconflow.cn
api-key: sk-necxquynovkphnulbbfbsopeosgogrsuqgxxctzhbcbpntyh
model: Qwen/Qwen3.5-27B
temperature: 0.2
max-tokens: 50000
max-iterations: 5
\ No newline at end of file
...@@ -13,8 +13,7 @@ spring: ...@@ -13,8 +13,7 @@ spring:
name: pms-agent name: pms-agent
ai: ai:
openai: openai:
api-key: 115e68c85d91d40dd3df42a06f08cbed api-key: 123
server: server:
servlet: servlet:
...@@ -34,3 +33,20 @@ jasypt: ...@@ -34,3 +33,20 @@ jasypt:
encryptor: encryptor:
algorithm: PBEWithMD5AndDES algorithm: PBEWithMD5AndDES
iv-generator-classname: org.jasypt.iv.NoIvGenerator iv-generator-classname: org.jasypt.iv.NoIvGenerator
pms:
care:
agent:
default-count: 10
max-requested-count: 500
user-page-size: 100
activity-page-size: 50
activity-candidate-limit: 40
user-stream-item-delay-millis: 250
conversation-ttl-minutes: 120
business:
base-url: http://localhost:8401
current-user-path: /union-js/api/functionCallTools/getCurrentUserInfo
eligible-activities-path: /union-js/api/functionCallTools/getActivityInfoList
user-search-path: /union-js/api/functionCallTools/getUserInfoList
activity-match-user-rules-path: /union-js/api/functionCallTools/getActivityMatchUserRules
...@@ -29,6 +29,7 @@ public class LLMChatTest { ...@@ -29,6 +29,7 @@ public class LLMChatTest {
private final ChatModel pmsChatModel; private final ChatModel pmsChatModel;
private final AiZhiYuProperties properties; private final AiZhiYuProperties properties;
@Autowired @Autowired
public LLMChatTest(ChatModel pmsChatModel, AiZhiYuProperties properties) { public LLMChatTest(ChatModel pmsChatModel, AiZhiYuProperties properties) {
this.pmsChatModel = pmsChatModel; this.pmsChatModel = pmsChatModel;
......
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