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