Files

1 line
22 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "9ao2hen/luca-pro", "files": {"luca-module-ai/luca-spring-boot-starter-ai/src/main/java/cn/mervyn/luca/framework/ai/core/model/deepseek/DeepSeekChatModel.java": {"c757bedb257b57d1a1f11c48a1904c149314f180": {"t": 1785746122.0487106, "n": 7441, "h": "285ece47127b4f0c", "c": "package cn.mervyn.luca.framework.ai.core.model.deepseek;\n\nimport cn.hutool.core.lang.Assert;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.ai.chat.metadata.ChatGenerationMetadata;\nimport org.springframework.ai.chat.model.ChatModel;\nimport org.springframework.ai.chat.model.ChatResponse;\nimport org.springframework.ai.chat.model.Generation;\nimport org.springframework.ai.chat.prompt.ChatOptions;\nimport org.springframework.ai.chat.prompt.Prompt;\nimport org.springframework.ai.model.ModelOptionsUtils;\nimport org.springframework.ai.openai.OpenAiChatOptions;\nimport org.springframework.ai.openai.api.OpenAiApi;\nimport org.springframework.ai.openai.metadata.OpenAiChatResponseMetadata;\nimport org.springframework.ai.retry.RetryUtils;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.retry.support.RetryTemplate;\nimport reactor.core.publisher.Flux;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static cn.mervyn.luca.framework.ai.core.model.deepseek.DeepSeekChatOptions.MODEL_DEFAULT;\n\n/**\n * DeepSeek {@link ChatModel} 实现类\n *\n * @author fansili\n */\n@Slf4j\npublic class DeepSeekChatModel implements ChatModel {\n\n private static final String BASE_URL = \"https://api.deepseek.com\";\n\n private final DeepSeekChatOptions defaultOptions;\n private final RetryTemplate retryTemplate;\n\n /**\n * DeepSeek 兼容 OpenAI 的 HTTP 接口,所以复用它的实现,简化接入成本\n *\n * 不过要注意,DeepSeek 没有完全兼容,所以不能使用 {@link org.springframework.ai.openai.OpenAiChatModel} 调用,但是实现会参考它\n */\n private final OpenAiApi openAiApi;\n\n public DeepSeekChatModel(String apiKey) {\n this(apiKey, DeepSeekChatOptions.builder().model(MODEL_DEFAULT).temperature(0.7F).build());\n }\n\n public DeepSeekChatModel(String apiKey, DeepSeekChatOptions options) {\n this(apiKey, options, RetryUtils.DEFAULT_RETRY_TEMPLATE);\n }\n\n public DeepSeekChatModel(String apiKey, DeepSeekChatOptions options, RetryTemplate retryTemplate) {\n Assert.notEmpty(apiKey, \"apiKey 不能为空\");\n Assert.notNull(options, \"options 不能为空\");\n Assert.notNull(retryTemplate, \"retryTemplate 不能为空\");\n this.openAiApi = new OpenAiApi(BASE_URL, apiKey);\n this.defaultOptions = options;\n this.retryTemplate = retryTemplate;\n }\n\n @Override\n public ChatResponse call(Prompt prompt) {\n OpenAiApi.ChatCompletionRequest request = createRequest(prompt, false);\n return this.retryTemplate.execute(ctx -> {\n // 1.1 发起调用\n ResponseEntity<OpenAiApi.ChatCompletion> completionEntity = openAiApi.chatCompletionEntity(request);\n // 1.2 校验结果\n OpenAiApi.ChatCompletion chatCompletion = completionEntity.getBody();\n if (chatCompletion == null) {\n log.warn(\"No chat completion returned for prompt: {}\", prompt);\n return new ChatResponse(List.of());\n }\n List<OpenAiApi.ChatCompletion.Choice> choices = chatCompletion.choices();\n if (choices == null) {\n log.warn(\"No choices returned for prompt: {}\", prompt);\n return new ChatResponse(List.of());\n }\n\n // 2. 转换 ChatResponse 返回\n List<Generation> generations = choices.stream().map(choice -> {\n Generation generation = new Generation(choice.message().content(), toMap(chatCompletion.id(), choice));\n if (choice.finishReason() != null) {\n generation.withGenerationMetadata(ChatGenerationMetadata.from(choice.finishReason().name(), null));\n }\n return generation;\n }).toList();\n return new ChatResponse(generations,\n OpenAiChatResponseMetadata.from(completionEntity.getBody()));\n });\n }\n\n private Map<String, Object> toMap(String id, OpenAiApi.ChatCompletion.Choice choice) {\n Map<String, Object> map = new HashMap<>();\n OpenAiApi.ChatCompletionMessage message = choice.message();\n if (message.role() != null) {\n map.put(\"role\", message.role().name());\n }\n if (choice.finishReason() != null) {\n map.put(\"finishReason\", choice.finishReason().name());\n }\n map.put(\"id\", id);\n return map;\n }\n\n @Override\n public Flux<ChatResponse> stream(Prompt prompt) {\n OpenAiApi.ChatCompletionRequest request = createRequest(prompt, true);\n return this.retryTemplate.execute(ctx -> {\n // 1. 发起调用\n Flux<OpenAiApi.ChatCompletionChunk> response = this.openAiApi.chatCompletionStream(request);\n return response.map(chatCompletion -> {\n String id = chatCompletion.id();\n // 2. 转换 ChatResponse 返回\n List<Generation> generations = chatCompletion.choices().stream().map(choice -> {\n String finish = (choice.finishReason() != null ? choice.finishReason().name() : \"\");\n String role = (choice.delta().role() != null ? choice.delta().role().name() : \"\");\n if (choice.finishReason() == OpenAiApi.ChatCompletionFinishReason.STOP) {\n // 兜底处理 DeepSeek 返回 STOP 时,role 为空的情况\n role = OpenAiApi.ChatCompletionMessage.Role.ASSISTANT.name();\n }\n Generation generation = new Generation(choice.delta().content(),\n Map.of(\"id\", id, \"role\", role, \"finishReason\", finish));\n if (choice.finishReason() != null) {\n generation = generation.withGenerationMetadata(\n ChatGenerationMetadata.from(choice.finishReason().name(), null));\n }\n return generation;\n }).toList();\n return new ChatResponse(generations);\n });\n });\n }\n\n OpenAiApi.ChatCompletionRequest createRequest(Prompt prompt, boolean stream) {\n // 1. 构建 ChatCompletionMessage 对象\n List<OpenAiApi.ChatCompletionMessage> chatCompletionMessages = prompt.getInstructions().stream().map(m ->\n new OpenAiApi.ChatCompletionMessage(m.getContent(), OpenAiApi.ChatCompletionMessage.Role.valueOf(m.getMessageType().name()))).toList();\n OpenAiApi.ChatCompletionRequest request = new OpenAiApi.ChatCompletionRequest(chatCompletionMessages, stream);\n\n // 2.1 补充 prompt 内置的 options\n if (prompt.getOptions() != null) {\n if (prompt.getOptions() instanceof ChatOptions runtimeOptions) {\n OpenAiChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions,\n ChatOptions.class, OpenAiChatOptions.class);\n request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, OpenAiApi.ChatCompletionRequest.class);\n } else {\n throw new IllegalArgumentException(\"Prompt options are not of type ChatOptions: \"\n + prompt.getOptions().getClass().getSimpleName());\n }\n }\n // 2.2 补充默认 options\n if (this.defaultOptions != null) {\n request = ModelOptionsUtils.merge(request, this.defaultOptions, OpenAiApi.ChatCompletionRequest.class);\n }\n return request;\n }\n\n @Override\n public ChatOptions getDefaultOptions() {\n return DeepSeekChatOptions.fromOptions(defaultOptions);\n }\n\n}\n"}, "7b4546b1e018dfec16618d144d663c3764f74cb2": {"t": 1785760008.7278576, "n": 7441, "h": "285ece47127b4f0c", "c": "package cn.mervyn.luca.framework.ai.core.model.deepseek;\n\nimport cn.hutool.core.lang.Assert;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.ai.chat.metadata.ChatGenerationMetadata;\nimport org.springframework.ai.chat.model.ChatModel;\nimport org.springframework.ai.chat.model.ChatResponse;\nimport org.springframework.ai.chat.model.Generation;\nimport org.springframework.ai.chat.prompt.ChatOptions;\nimport org.springframework.ai.chat.prompt.Prompt;\nimport org.springframework.ai.model.ModelOptionsUtils;\nimport org.springframework.ai.openai.OpenAiChatOptions;\nimport org.springframework.ai.openai.api.OpenAiApi;\nimport org.springframework.ai.openai.metadata.OpenAiChatResponseMetadata;\nimport org.springframework.ai.retry.RetryUtils;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.retry.support.RetryTemplate;\nimport reactor.core.publisher.Flux;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static cn.mervyn.luca.framework.ai.core.model.deepseek.DeepSeekChatOptions.MODEL_DEFAULT;\n\n/**\n * DeepSeek {@link ChatModel} 实现类\n *\n * @author fansili\n */\n@Slf4j\npublic class DeepSeekChatModel implements ChatModel {\n\n private static final String BASE_URL = \"https://api.deepseek.com\";\n\n private final DeepSeekChatOptions defaultOptions;\n private final RetryTemplate retryTemplate;\n\n /**\n * DeepSeek 兼容 OpenAI 的 HTTP 接口,所以复用它的实现,简化接入成本\n *\n * 不过要注意,DeepSeek 没有完全兼容,所以不能使用 {@link org.springframework.ai.openai.OpenAiChatModel} 调用,但是实现会参考它\n */\n private final OpenAiApi openAiApi;\n\n public DeepSeekChatModel(String apiKey) {\n this(apiKey, DeepSeekChatOptions.builder().model(MODEL_DEFAULT).temperature(0.7F).build());\n }\n\n public DeepSeekChatModel(String apiKey, DeepSeekChatOptions options) {\n this(apiKey, options, RetryUtils.DEFAULT_RETRY_TEMPLATE);\n }\n\n public DeepSeekChatModel(String apiKey, DeepSeekChatOptions options, RetryTemplate retryTemplate) {\n Assert.notEmpty(apiKey, \"apiKey 不能为空\");\n Assert.notNull(options, \"options 不能为空\");\n Assert.notNull(retryTemplate, \"retryTemplate 不能为空\");\n this.openAiApi = new OpenAiApi(BASE_URL, apiKey);\n this.defaultOptions = options;\n this.retryTemplate = retryTemplate;\n }\n\n @Override\n public ChatResponse call(Prompt prompt) {\n OpenAiApi.ChatCompletionRequest request = createRequest(prompt, false);\n return this.retryTemplate.execute(ctx -> {\n // 1.1 发起调用\n ResponseEntity<OpenAiApi.ChatCompletion> completionEntity = openAiApi.chatCompletionEntity(request);\n // 1.2 校验结果\n OpenAiApi.ChatCompletion chatCompletion = completionEntity.getBody();\n if (chatCompletion == null) {\n log.warn(\"No chat completion returned for prompt: {}\", prompt);\n return new ChatResponse(List.of());\n }\n List<OpenAiApi.ChatCompletion.Choice> choices = chatCompletion.choices();\n if (choices == null) {\n log.warn(\"No choices returned for prompt: {}\", prompt);\n return new ChatResponse(List.of());\n }\n\n // 2. 转换 ChatResponse 返回\n List<Generation> generations = choices.stream().map(choice -> {\n Generation generation = new Generation(choice.message().content(), toMap(chatCompletion.id(), choice));\n if (choice.finishReason() != null) {\n generation.withGenerationMetadata(ChatGenerationMetadata.from(choice.finishReason().name(), null));\n }\n return generation;\n }).toList();\n return new ChatResponse(generations,\n OpenAiChatResponseMetadata.from(completionEntity.getBody()));\n });\n }\n\n private Map<String, Object> toMap(String id, OpenAiApi.ChatCompletion.Choice choice) {\n Map<String, Object> map = new HashMap<>();\n OpenAiApi.ChatCompletionMessage message = choice.message();\n if (message.role() != null) {\n map.put(\"role\", message.role().name());\n }\n if (choice.finishReason() != null) {\n map.put(\"finishReason\", choice.finishReason().name());\n }\n map.put(\"id\", id);\n return map;\n }\n\n @Override\n public Flux<ChatResponse> stream(Prompt prompt) {\n OpenAiApi.ChatCompletionRequest request = createRequest(prompt, true);\n return this.retryTemplate.execute(ctx -> {\n // 1. 发起调用\n Flux<OpenAiApi.ChatCompletionChunk> response = this.openAiApi.chatCompletionStream(request);\n return response.map(chatCompletion -> {\n String id = chatCompletion.id();\n // 2. 转换 ChatResponse 返回\n List<Generation> generations = chatCompletion.choices().stream().map(choice -> {\n String finish = (choice.finishReason() != null ? choice.finishReason().name() : \"\");\n String role = (choice.delta().role() != null ? choice.delta().role().name() : \"\");\n if (choice.finishReason() == OpenAiApi.ChatCompletionFinishReason.STOP) {\n // 兜底处理 DeepSeek 返回 STOP 时,role 为空的情况\n role = OpenAiApi.ChatCompletionMessage.Role.ASSISTANT.name();\n }\n Generation generation = new Generation(choice.delta().content(),\n Map.of(\"id\", id, \"role\", role, \"finishReason\", finish));\n if (choice.finishReason() != null) {\n generation = generation.withGenerationMetadata(\n ChatGenerationMetadata.from(choice.finishReason().name(), null));\n }\n return generation;\n }).toList();\n return new ChatResponse(generations);\n });\n });\n }\n\n OpenAiApi.ChatCompletionRequest createRequest(Prompt prompt, boolean stream) {\n // 1. 构建 ChatCompletionMessage 对象\n List<OpenAiApi.ChatCompletionMessage> chatCompletionMessages = prompt.getInstructions().stream().map(m ->\n new OpenAiApi.ChatCompletionMessage(m.getContent(), OpenAiApi.ChatCompletionMessage.Role.valueOf(m.getMessageType().name()))).toList();\n OpenAiApi.ChatCompletionRequest request = new OpenAiApi.ChatCompletionRequest(chatCompletionMessages, stream);\n\n // 2.1 补充 prompt 内置的 options\n if (prompt.getOptions() != null) {\n if (prompt.getOptions() instanceof ChatOptions runtimeOptions) {\n OpenAiChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions,\n ChatOptions.class, OpenAiChatOptions.class);\n request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, OpenAiApi.ChatCompletionRequest.class);\n } else {\n throw new IllegalArgumentException(\"Prompt options are not of type ChatOptions: \"\n + prompt.getOptions().getClass().getSimpleName());\n }\n }\n // 2.2 补充默认 options\n if (this.defaultOptions != null) {\n request = ModelOptionsUtils.merge(request, this.defaultOptions, OpenAiApi.ChatCompletionRequest.class);\n }\n return request;\n }\n\n @Override\n public ChatOptions getDefaultOptions() {\n return DeepSeekChatOptions.fromOptions(defaultOptions);\n }\n\n}\n"}}, "luca-module-ai/luca-spring-boot-starter-ai/src/test/java/cn/mervyn/luca/framework/ai/chat/DeepSeekChatModelTests.java": {"c757bedb257b57d1a1f11c48a1904c149314f180": {"t": 1785747155.8321664, "n": 1583, "h": "1066bd866a795585", "c": "package cn.mervyn.luca.framework.ai.chat;\n\nimport cn.mervyn.luca.framework.ai.core.model.deepseek.DeepSeekChatModel;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.ai.chat.messages.Message;\nimport org.springframework.ai.chat.messages.SystemMessage;\nimport org.springframework.ai.chat.messages.UserMessage;\nimport org.springframework.ai.chat.model.ChatResponse;\nimport org.springframework.ai.chat.prompt.Prompt;\nimport reactor.core.publisher.Flux;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * {@link DeepSeekChatModel} 集成测试\n *\n * @author 9ao2hen\n */\npublic class DeepSeekChatModelTests {\n\n private final DeepSeekChatModel chatModel = new DeepSeekChatModel(\"sk-e94db327cc7d457d99a8de8810fc6b12\");\n\n @Test\n @Disabled\n public void testCall() {\n // 准备参数\n List<Message> messages = new ArrayList<>();\n messages.add(new SystemMessage(\"你是一个优质的文言文作者,用文言文描述着各城市的人文风景。\"));\n messages.add(new UserMessage(\"1 + 1 = ?\"));\n\n // 调用\n ChatResponse response = chatModel.call(new Prompt(messages));\n // 打印结果\n System.out.println(response);\n }\n\n @Test\n @Disabled\n public void testStream() {\n // 准备参数\n List<Message> messages = new ArrayList<>();\n messages.add(new SystemMessage(\"你是一个优质的文言文作者,用文言文描述着各城市的人文风景。\"));\n messages.add(new UserMessage(\"1 + 1 = ?\"));\n\n // 调用\n Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));\n // 打印结果\n flux.doOnNext(System.out::println).then().block();\n }\n\n}\n"}, "70930cc4597d834048507eb285c7b7b7b0431e0c": {"t": 1785760009.3686285, "n": 1583, "h": "1066bd866a795585", "c": "package cn.mervyn.luca.framework.ai.chat;\n\nimport cn.mervyn.luca.framework.ai.core.model.deepseek.DeepSeekChatModel;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.ai.chat.messages.Message;\nimport org.springframework.ai.chat.messages.SystemMessage;\nimport org.springframework.ai.chat.messages.UserMessage;\nimport org.springframework.ai.chat.model.ChatResponse;\nimport org.springframework.ai.chat.prompt.Prompt;\nimport reactor.core.publisher.Flux;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * {@link DeepSeekChatModel} 集成测试\n *\n * @author 9ao2hen\n */\npublic class DeepSeekChatModelTests {\n\n private final DeepSeekChatModel chatModel = new DeepSeekChatModel(\"sk-e94db327cc7d457d99a8de8810fc6b12\");\n\n @Test\n @Disabled\n public void testCall() {\n // 准备参数\n List<Message> messages = new ArrayList<>();\n messages.add(new SystemMessage(\"你是一个优质的文言文作者,用文言文描述着各城市的人文风景。\"));\n messages.add(new UserMessage(\"1 + 1 = ?\"));\n\n // 调用\n ChatResponse response = chatModel.call(new Prompt(messages));\n // 打印结果\n System.out.println(response);\n }\n\n @Test\n @Disabled\n public void testStream() {\n // 准备参数\n List<Message> messages = new ArrayList<>();\n messages.add(new SystemMessage(\"你是一个优质的文言文作者,用文言文描述着各城市的人文风景。\"));\n messages.add(new UserMessage(\"1 + 1 = ?\"));\n\n // 调用\n Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));\n // 打印结果\n flux.doOnNext(System.out::println).then().block();\n }\n\n}\n"}, "7b4546b1e018dfec16618d144d663c3764f74cb2": {"t": 1785760011.0089662, "n": 1580, "h": "83b20e6c7d84c841", "c": "package cn.mervyn.luca.framework.ai.chat;\n\nimport cn.mervyn.luca.framework.ai.core.model.deepseek.DeepSeekChatModel;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.ai.chat.messages.Message;\nimport org.springframework.ai.chat.messages.SystemMessage;\nimport org.springframework.ai.chat.messages.UserMessage;\nimport org.springframework.ai.chat.model.ChatResponse;\nimport org.springframework.ai.chat.prompt.Prompt;\nimport reactor.core.publisher.Flux;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * {@link DeepSeekChatModel} 集成测试\n *\n * @author 芋道源码\n */\npublic class DeepSeekChatModelTests {\n\n private final DeepSeekChatModel chatModel = new DeepSeekChatModel(\"sk-e94db327cc7d457d99a8de8810fc6b12\");\n\n @Test\n @Disabled\n public void testCall() {\n // 准备参数\n List<Message> messages = new ArrayList<>();\n messages.add(new SystemMessage(\"你是一个优质的文言文作者,用文言文描述着各城市的人文风景。\"));\n messages.add(new UserMessage(\"1 + 1 = ?\"));\n\n // 调用\n ChatResponse response = chatModel.call(new Prompt(messages));\n // 打印结果\n System.out.println(response);\n }\n\n @Test\n @Disabled\n public void testStream() {\n // 准备参数\n List<Message> messages = new ArrayList<>();\n messages.add(new SystemMessage(\"你是一个优质的文言文作者,用文言文描述着各城市的人文风景。\"));\n messages.add(new UserMessage(\"1 + 1 = ?\"));\n\n // 调用\n Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));\n // 打印结果\n flux.doOnNext(System.out::println).then().block();\n }\n\n}\n"}}}}