diff --git a/pom.xml b/pom.xml
index 7054ec9..5fed99c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -55,14 +55,28 @@
com.alibaba.cloud.ai
spring-ai-alibaba-starter-dashscope
-
-
-
+
+
+ org.springframework.ai
+ spring-ai-starter-model-chat-memory-repository-cassandra
+
org.springframework.boot
spring-boot-starter-test
test
+
+
+ com.alibaba
+ dashscope-sdk-java
+ 2.21.13
+
+
+ org.slf4j
+ slf4j-simple
+
+
+
org.apache.commons
diff --git a/src/main/java/com/hanserwei/airobot/config/ChatClientConfig.java b/src/main/java/com/hanserwei/airobot/config/ChatClientConfig.java
index b72373b..a3980f9 100644
--- a/src/main/java/com/hanserwei/airobot/config/ChatClientConfig.java
+++ b/src/main/java/com/hanserwei/airobot/config/ChatClientConfig.java
@@ -1,5 +1,6 @@
package com.hanserwei.airobot.config;
+import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.hanserwei.airobot.advisor.MyLoggerAdvisor;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
@@ -22,11 +23,13 @@ public class ChatClientConfig {
* @return ChatClient
*/
@Bean
- public ChatClient chatClient(DeepSeekChatModel chatModel) {
+ public ChatClient chatClient(DashScopeChatModel chatModel) {
return ChatClient.builder(chatModel)
- .defaultSystem("请你扮演一名犬小哈 Java 项目实战专栏的客服人员")
- .defaultAdvisors(new SimpleLoggerAdvisor(),
- new MyLoggerAdvisor())
+// .defaultSystem("请你扮演一名犬小哈 Java 项目实战专栏的客服人员")
+ .defaultAdvisors(
+ new SimpleLoggerAdvisor(),
+ new MyLoggerAdvisor(),
+ MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
}
\ No newline at end of file
diff --git a/src/main/java/com/hanserwei/airobot/config/ChatMemoryConfig.java b/src/main/java/com/hanserwei/airobot/config/ChatMemoryConfig.java
index d4f00a5..48158ff 100644
--- a/src/main/java/com/hanserwei/airobot/config/ChatMemoryConfig.java
+++ b/src/main/java/com/hanserwei/airobot/config/ChatMemoryConfig.java
@@ -4,6 +4,7 @@ import jakarta.annotation.Resource;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
+import org.springframework.ai.chat.memory.repository.cassandra.CassandraChatMemoryRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -14,7 +15,7 @@ public class ChatMemoryConfig {
* 记忆存储
*/
@Resource
- private ChatMemoryRepository chatMemoryRepository;
+ private CassandraChatMemoryRepository chatMemoryRepository;
/**
* 初始化一个 ChatMemory 实例,并注入到 Spring 容器中
diff --git a/src/main/java/com/hanserwei/airobot/controller/MultimodalityController.java b/src/main/java/com/hanserwei/airobot/controller/MultimodalityController.java
new file mode 100644
index 0000000..926ee05
--- /dev/null
+++ b/src/main/java/com/hanserwei/airobot/controller/MultimodalityController.java
@@ -0,0 +1,64 @@
+package com.hanserwei.airobot.controller;
+
+import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
+import jakarta.annotation.Resource;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.content.Media;
+import org.springframework.ai.openai.OpenAiChatModel;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.util.MimeTypeUtils;
+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;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/v9/ai")
+public class MultimodalityController {
+
+ @Resource
+ private DashScopeChatModel chatModel;
+
+ /**
+ * 流式对话
+ * @param message
+ * @return
+ */
+ @GetMapping(value = "/generateStream", produces = "text/html;charset=utf-8")
+ public Flux generateStream(@RequestParam(value = "message") String message) {
+ // 1. 创建媒体资源
+ Media image = new Media(
+ MimeTypeUtils.IMAGE_PNG,
+ new ClassPathResource("/images/img.png")
+ );
+
+ // 2. 附加选项(可选),如温度值等等
+ Map metadata = new HashMap<>();
+ metadata.put("temperature", 0.7);
+
+ // 3. 构建多模态消息
+ UserMessage userMessage = UserMessage.builder()
+ .text(message)
+ .media(image)
+ .metadata(metadata)
+ .build();
+
+ // 4. 构建提示词
+ Prompt prompt = new Prompt(List.of(userMessage));
+ // 5. 流式调用
+ return chatModel.stream(prompt)
+ .mapNotNull(chatResponse -> {
+ Generation generation = chatResponse.getResult();
+ return generation.getOutput().getText();
+ });
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/hanserwei/airobot/controller/PromptTemplateController.java b/src/main/java/com/hanserwei/airobot/controller/PromptTemplateController.java
index 43997d9..4e5b955 100644
--- a/src/main/java/com/hanserwei/airobot/controller/PromptTemplateController.java
+++ b/src/main/java/com/hanserwei/airobot/controller/PromptTemplateController.java
@@ -8,7 +8,6 @@ import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
-import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.template.st.StTemplateRenderer;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
diff --git a/src/main/java/com/hanserwei/airobot/controller/StructuredOutputController.java b/src/main/java/com/hanserwei/airobot/controller/StructuredOutputController.java
new file mode 100644
index 0000000..0bba37e
--- /dev/null
+++ b/src/main/java/com/hanserwei/airobot/controller/StructuredOutputController.java
@@ -0,0 +1,127 @@
+package com.hanserwei.airobot.controller;
+
+import com.hanserwei.airobot.model.ActorFilmography;
+import com.hanserwei.airobot.model.Book;
+import jakarta.annotation.Resource;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.chat.prompt.PromptTemplate;
+import org.springframework.ai.converter.BeanOutputConverter;
+import org.springframework.ai.converter.ListOutputConverter;
+import org.springframework.ai.converter.MapOutputConverter;
+import org.springframework.core.convert.support.DefaultConversionService;
+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 java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/v8/ai")
+public class StructuredOutputController {
+
+ @Resource
+ private ChatClient chatClient;
+
+ /**
+ * 示例1: BeanOutputConverter - 获取演员电影作品集
+ *
+ * @param name
+ * @return
+ */
+ @GetMapping("/actor/films")
+ public ActorFilmography generate(@RequestParam(value = "name") String name) {
+ // 一次性返回结果
+ return chatClient.prompt()
+ .user(u -> u.text("""
+ 请为演员 {actor} 生成包含5部代表作的电影作品集,
+ 只包含 {actor} 担任主演的电影,不要包含任何解释说明。
+ """)
+ .param("actor", name))
+ .call()
+ .entity(ActorFilmography.class);
+ }
+
+ /**
+ * 示例2: MapOutputConverter - 获取编程语言信息
+ * @param language
+ * @return
+ */
+ @GetMapping("/language-info")
+ public Map getLanguageInfo(@RequestParam(value = "lang") String language) {
+
+ String userText = """
+ 请提供关于编程语言 {language} 的结构化信息,包含以下字段:"
+ name (语言名称), "
+ popularity (流行度排名,整数), "
+ features (主要特性,字符串数组), "
+ releaseYear (首次发布年份). "
+ 不要包含任何解释说明,直接输出 JSON 格式数据。
+ """;
+
+ return chatClient.prompt()
+ .user(u -> u.text(userText).param("language", language))
+ .call()
+ .entity(new MapOutputConverter());
+ }
+
+ /**
+ * 示例3: ListOutputConverter - 获取城市列表
+ * @param country
+ * @return
+ */
+ @GetMapping("/city-list")
+ public List getCityList(@RequestParam(value = "country") String country) {
+
+ return chatClient.prompt()
+ .user(u -> u.text(
+ """
+ 列出 {country} 的8个主要城市名称。
+ 不要包含任何编号、解释或其他文本,直接输出城市名称列表。
+ """)
+ .param("country", country))
+ .call()
+ .entity(new ListOutputConverter(new DefaultConversionService()));
+ }
+
+ /**
+ * 使用低级 API 的 BeanOutputConverter - 获取书籍信息
+ * @param bookTitle
+ * @return
+ */
+ @GetMapping("/book-info")
+ public Book getBookInfo(@RequestParam(value = "name") String bookTitle) {
+
+ // 使用 BeanOutputConverter 定义输出格式
+ BeanOutputConverter converter = new BeanOutputConverter<>(Book.class);
+
+ // 提示词模板
+ String template = """
+ 请提供关于书籍《{bookTitle}》的详细信息:
+ 1. 作者姓名
+ 2. 出版年份
+ 3. 主要类型(数组)
+ 4. 书籍描述(不少于50字)
+
+ 不要包含任何解释说明,直接按指定格式输出。
+ {format}
+ """;
+
+ // 创建 Prompt
+ PromptTemplate promptTemplate = new PromptTemplate(template);
+ Prompt prompt = promptTemplate.create(Map.of(
+ "bookTitle", bookTitle,
+ "format", converter.getFormat()
+ ));
+
+ // 调用模型并转换结果
+ String result = chatClient.prompt(prompt)
+ .call()
+ .content();
+
+ // 结构化转换
+ return converter.convert(result);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/hanserwei/airobot/controller/Text2ImgController.java b/src/main/java/com/hanserwei/airobot/controller/Text2ImgController.java
new file mode 100644
index 0000000..e2f9306
--- /dev/null
+++ b/src/main/java/com/hanserwei/airobot/controller/Text2ImgController.java
@@ -0,0 +1,54 @@
+package com.hanserwei.airobot.controller;
+
+import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis;
+import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam;
+import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult;
+import com.alibaba.dashscope.exception.ApiException;
+import com.alibaba.dashscope.exception.NoApiKeyException;
+import com.alibaba.dashscope.utils.JsonUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+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;
+
+@RestController
+@RequestMapping("/v10/ai")
+@Slf4j
+public class Text2ImgController {
+
+ @Value("${spring.ai.dashscope.api-key}")
+ private String apiKey;
+
+ /**
+ * 调用阿里百炼图生文大模型
+ * @param prompt 提示词
+ * @return
+ */
+ @GetMapping("/text2img")
+ public String text2Image(@RequestParam(value = "prompt") String prompt) {
+ // 构建文生图参数
+ ImageSynthesisParam param = ImageSynthesisParam.builder()
+ .apiKey(apiKey) // 阿里百炼 API Key
+ .model("wanx2.1-t2i-plus") // 模型名称
+ .prompt(prompt) // 提示词
+ .n(1) // 生成图片的数量,这里指定为一张
+ .size("1024*1024") // 输出图像的分辨率
+ .build();
+
+ // 同步调用 AI 大模型,生成图片
+ ImageSynthesis imageSynthesis = new ImageSynthesis();
+ ImageSynthesisResult result = null;
+ try {
+ log.info("## 同步调用,请稍等一会...");
+ result = imageSynthesis.call(param);
+ } catch (ApiException | NoApiKeyException e){
+ log.error("", e);
+ }
+
+ // 返回生成的结果(包含图片的 URL 链接)
+ return JsonUtils.toJson(result);
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/hanserwei/airobot/model/ActorFilmography.java b/src/main/java/com/hanserwei/airobot/model/ActorFilmography.java
new file mode 100644
index 0000000..1ff7866
--- /dev/null
+++ b/src/main/java/com/hanserwei/airobot/model/ActorFilmography.java
@@ -0,0 +1,9 @@
+package com.hanserwei.airobot.model;
+
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+import java.util.List;
+
+@JsonPropertyOrder({"actor", "movies"})
+public record ActorFilmography(String actor, List movies) {
+}
\ No newline at end of file
diff --git a/src/main/java/com/hanserwei/airobot/model/Book.java b/src/main/java/com/hanserwei/airobot/model/Book.java
new file mode 100644
index 0000000..620ed46
--- /dev/null
+++ b/src/main/java/com/hanserwei/airobot/model/Book.java
@@ -0,0 +1,37 @@
+package com.hanserwei.airobot.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Book {
+ /**
+ * 书名
+ */
+ private String title;
+
+ /**
+ * 作者
+ */
+ private String author;
+
+ /**
+ * 发布年份
+ */
+ private Integer publishYear;
+
+ /**
+ * 类型
+ */
+ private List genres;
+
+ /**
+ * 简介
+ */
+ private String description;
+}
\ No newline at end of file
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 65068d9..d5b7b08 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -2,6 +2,10 @@
spring:
application:
name: han-ai-robot-springboot
+ cassandra:
+ contact-points: 127.0.0.1 # Cassandra 集群节点地址(可配置多个,用逗号分隔)
+ port: 9042 # 端口号
+ local-datacenter: datacenter1 # 必须与集群配置的数据中心名称一致(大小写敏感)
ai:
deepseek:
api-key: ENC(MROXdiEHmWk08koE63bTzFqW52MaXLpMkM9Cyl40Ubj+Lw1yKeZuHLEcs6jTFY8ditY4gJ1365LMAY8Z9G1uwfYFYaYdb3NyijplX7GuDZA=) # 填写 DeepSeek Api Key, 改成你自己的
@@ -34,8 +38,17 @@ spring:
api-key: ENC(cMgcKZkFllyE88DIbGwLKot9Vg02co+gsmY8L8o4/o3UjhcmqO4lJzFU35Sx0n+qFG8pDL0wBjoWrT8X6BuRw9vNlQhY1LgRWHaF9S1zzyM=)
chat:
options:
- model: qwen-plus
+ model: qwen-omni-turbo
temperature: 0.7
+ multi-model: true
+ chat:
+ memory:
+ repository:
+ cassandra:
+ keyspace: han_ai_robot
+ table: t_ai_chat_memory
+ time-to-live: 1095d
+ initialize-schema: true
jasypt:
encryptor:
diff --git a/src/main/resources/images/img.png b/src/main/resources/images/img.png
new file mode 100644
index 0000000..29b0848
Binary files /dev/null and b/src/main/resources/images/img.png differ