dev #6
| @ -79,7 +79,7 @@ jobs: | ||||
|           ./gradlew jib --stacktrace --build-cache --info -Dorg.gradle.caching=true -Dorg.gradle.jvmargs="-Xmx2g -Xms2g -XX:MaxMetaspaceSize=1g" | tee build.log | ||||
|            | ||||
|           echo "=== 镜像构建结果 ===" | ||||
|           docker images | grep ${{env.PROJECT_NAME}} | ||||
|           docker images | grep ${{ env.PROJECT_NAME }} || true | ||||
|       - name: 🛑 Stop Gradle Daemon | ||||
|         run: | | ||||
|           echo "停止Gradle守护进程..." | ||||
|  | ||||
| @ -23,6 +23,7 @@ allprojects { | ||||
|         set(ProjectFlags.USE_LLM_CORE_SPI, false) | ||||
|         set(ProjectFlags.USE_SPRING_CLOUD_BOM, false) | ||||
|         set(ProjectFlags.IS_ROOT_MODULE, false) | ||||
|         set(ProjectFlags.USE_SPRING_BOOT_BOM,false) | ||||
|     } | ||||
|     repositories { | ||||
|         mavenLocal() | ||||
| @ -93,6 +94,11 @@ subprojects { | ||||
|                 implementation(platform(libs.springCloudDependencies.bom)) | ||||
|             } | ||||
|         } | ||||
|         if(project.getPropertyByBoolean(ProjectFlags.USE_SPRING_BOOT_BOM)){ | ||||
|             dependencies { | ||||
|                 implementation(platform(libs.springBootDependencies.bom)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     val libs = rootProject.libs | ||||
|     apply { | ||||
|  | ||||
| @ -14,6 +14,8 @@ object Modules { | ||||
|    object Core{ | ||||
|        // llmx-core-spi模块的路径,用于定义核心功能的SPI | ||||
|        const val SPI = ":llmx-core:llmx-core-spi" | ||||
|        const val API = ":llmx-core:llmx-core-api" | ||||
|        const val COMMON = ":llmx-core:llmx-core-common" | ||||
|    } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| object ProjectFlags { | ||||
|     const val USE_SPRING_BOOT_WEB = "useSpringBootWeb" | ||||
|     const val USE_SPRING_BOOT_BOM = "useSpringBootBom" | ||||
|     const val USE_SPRING_CLOUD_BOM = "useSpringCloudBom" | ||||
|     const val USE_LLM_CORE_SPI = "useLLMCoreSPI" | ||||
|     const val IS_ROOT_MODULE = "isRootModule" | ||||
|  | ||||
| @ -8,6 +8,7 @@ spring-cloud-starter-alibaba-nacos-discovery-version = "2023.0.1.0" | ||||
| forgeBoot-version = "1.1.0-SNAPSHOT" | ||||
| okHttp-version = "4.12.0" | ||||
| jib-version = "3.4.2" | ||||
| org-reactivestreams-reactiveStreams-version = "1.0.4" | ||||
| [plugins] | ||||
| # 应用 Java 插件,提供基本的 Java 代码编译和构建能力 | ||||
| java = { id = "java" } | ||||
| @ -31,6 +32,7 @@ jibLocalPlugin = { id = "org.jcnc.llmx.plugin.jib" } | ||||
| jib-gradlePlugin = { module = "com.google.cloud.tools.jib:com.google.cloud.tools.jib.gradle.plugin", version.ref = "jib-version" } | ||||
| # bom | ||||
| springCloudDependencies-bom = { group ="org.springframework.cloud",name = "spring-cloud-dependencies", version.ref = "spring-cloud-version" } | ||||
| springBootDependencies-bom = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "spring-boot-version" } | ||||
| # kotlinx | ||||
| # 响应式协程库 | ||||
| kotlinx-coruntes-reactor = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-reactor" } | ||||
| @ -41,6 +43,7 @@ aliyun-bailian = { group = "com.aliyun", name = "bailian20231229", version.ref = | ||||
| # SrpingCloud | ||||
| springCloudStarter-alibaba-nacos-discovery = { group = "com.alibaba.cloud", name = "spring-cloud-starter-alibaba-nacos-discovery", version.ref = "spring-cloud-starter-alibaba-nacos-discovery-version" } | ||||
| springCloudStarter-loadbalancer = { group = "org.springframework.cloud", name = "spring-cloud-starter-loadbalancer" } | ||||
| springCloudStarter-openfeign = { group = "org.springframework.cloud", name = "spring-cloud-starter-openfeign" } | ||||
| 
 | ||||
| # SpringBootStarter | ||||
| springBootStarter-webflux = { group = "org.springframework.boot", name = "spring-boot-starter-webflux" } | ||||
| @ -49,6 +52,8 @@ springBootStarter-test = { group = "org.springframework.boot", name = "spring-bo | ||||
| 
 | ||||
| junitPlatform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" } | ||||
| 
 | ||||
| # org-reactivestreams | ||||
| org-reactivestreams-reactiveStreams = { group = "org.reactivestreams", name = "reactive-streams" ,version.ref="org-reactivestreams-reactiveStreams-version"} | ||||
| # OkHttp | ||||
| okHttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okHttp-version" } | ||||
| # forgeBoot | ||||
|  | ||||
							
								
								
									
										2
									
								
								gradlew
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								gradlew
									
									
									
									
										vendored
									
									
								
							| @ -207,7 +207,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' | ||||
| # Collect all arguments for the java command: | ||||
| #   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, | ||||
| #     and any embedded shellness will be escaped. | ||||
| #   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be | ||||
| #   * For gewuyou: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be | ||||
| #     treated as '${Hostname}' itself on the command line. | ||||
| 
 | ||||
| set -- \ | ||||
|  | ||||
							
								
								
									
										3
									
								
								llmx-app/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								llmx-app/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| /gradlew text eol=lf | ||||
| *.bat text eol=crlf | ||||
| *.jar binary | ||||
							
								
								
									
										40
									
								
								llmx-app/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								llmx-app/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| HELP.md | ||||
| .gradle | ||||
| build/ | ||||
| !gradle/wrapper/gradle-wrapper.jar | ||||
| !**/src/main/**/build/ | ||||
| !**/src/test/**/build/ | ||||
| 
 | ||||
| ### STS ### | ||||
| .apt_generated | ||||
| .classpath | ||||
| .factorypath | ||||
| .project | ||||
| .settings | ||||
| .springBeans | ||||
| .sts4-cache | ||||
| bin/ | ||||
| !**/src/main/**/bin/ | ||||
| !**/src/test/**/bin/ | ||||
| 
 | ||||
| ### IntelliJ IDEA ### | ||||
| .idea | ||||
| *.iws | ||||
| *.iml | ||||
| *.ipr | ||||
| out/ | ||||
| !**/src/main/**/out/ | ||||
| !**/src/test/**/out/ | ||||
| 
 | ||||
| ### NetBeans ### | ||||
| /nbproject/private/ | ||||
| /nbbuild/ | ||||
| /dist/ | ||||
| /nbdist/ | ||||
| /.nb-gradle/ | ||||
| 
 | ||||
| ### VS Code ### | ||||
| .vscode/ | ||||
| 
 | ||||
| ### Kotlin ### | ||||
| .kotlin | ||||
							
								
								
									
										9
									
								
								llmx-app/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								llmx-app/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| dependencies { | ||||
| 
 | ||||
| } | ||||
| /** | ||||
|  * 由于 Kotlin 插件引入时会自动添加依赖,但根项目不需要 Kotlin 依赖,因此需要排除 Kotlin 依赖 | ||||
|  */ | ||||
| configurations.implementation { | ||||
|     exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib") | ||||
| } | ||||
							
								
								
									
										3
									
								
								llmx-app/llmx-app-multimodality/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								llmx-app/llmx-app-multimodality/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| /gradlew text eol=lf | ||||
| *.bat text eol=crlf | ||||
| *.jar binary | ||||
							
								
								
									
										40
									
								
								llmx-app/llmx-app-multimodality/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								llmx-app/llmx-app-multimodality/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| HELP.md | ||||
| .gradle | ||||
| build/ | ||||
| !gradle/wrapper/gradle-wrapper.jar | ||||
| !**/src/main/**/build/ | ||||
| !**/src/test/**/build/ | ||||
| 
 | ||||
| ### STS ### | ||||
| .apt_generated | ||||
| .classpath | ||||
| .factorypath | ||||
| .project | ||||
| .settings | ||||
| .springBeans | ||||
| .sts4-cache | ||||
| bin/ | ||||
| !**/src/main/**/bin/ | ||||
| !**/src/test/**/bin/ | ||||
| 
 | ||||
| ### IntelliJ IDEA ### | ||||
| .idea | ||||
| *.iws | ||||
| *.iml | ||||
| *.ipr | ||||
| out/ | ||||
| !**/src/main/**/out/ | ||||
| !**/src/test/**/out/ | ||||
| 
 | ||||
| ### NetBeans ### | ||||
| /nbproject/private/ | ||||
| /nbbuild/ | ||||
| /dist/ | ||||
| /nbdist/ | ||||
| /.nb-gradle/ | ||||
| 
 | ||||
| ### VS Code ### | ||||
| .vscode/ | ||||
| 
 | ||||
| ### Kotlin ### | ||||
| .kotlin | ||||
							
								
								
									
										13
									
								
								llmx-app/llmx-app-multimodality/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								llmx-app/llmx-app-multimodality/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| extra { | ||||
|     // 开启springboot | ||||
|     setProperty(ProjectFlags.USE_SPRING_BOOT_WEB, true) | ||||
|     setProperty(ProjectFlags.USE_SPRING_CLOUD_BOM, true) | ||||
| } | ||||
| dependencies { | ||||
|     // Nacos 服务发现和配置 | ||||
|     implementation(libs.springCloudStarter.alibaba.nacos.discovery) | ||||
|     implementation(libs.forgeBoot.core.extension) | ||||
|     implementation(libs.forgeBoot.webmvc.version.springBootStarter) | ||||
|     implementation(libs.jackson.module.kotlin) | ||||
|     implementation(project(Modules.Core.API)) | ||||
| } | ||||
| @ -0,0 +1,11 @@ | ||||
| package org.jcnc.llmx.app.multimodality | ||||
| 
 | ||||
| import org.springframework.boot.autoconfigure.SpringBootApplication | ||||
| import org.springframework.boot.runApplication | ||||
| 
 | ||||
| @SpringBootApplication | ||||
| class LlmxAppMultimodalityApplication | ||||
| 
 | ||||
| fun main(args: Array<String>) { | ||||
| 	runApplication<LlmxAppMultimodalityApplication>(*args) | ||||
| } | ||||
| @ -0,0 +1,8 @@ | ||||
| server: | ||||
|   port: 8081 | ||||
| spring: | ||||
|   config: | ||||
|     import: classpath:bootstrap-dev.yml | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @ -0,0 +1,7 @@ | ||||
| server: | ||||
|   port: 9003 | ||||
| spring: | ||||
|   config: | ||||
|     import: classpath:bootstrap-test.yml | ||||
| 
 | ||||
| 
 | ||||
| @ -0,0 +1,5 @@ | ||||
| spring: | ||||
|   application: | ||||
|     name: llmx-app-multimodality | ||||
|   profiles: | ||||
|     active: dev | ||||
| @ -0,0 +1,12 @@ | ||||
| spring: | ||||
|   cloud: | ||||
|     nacos: | ||||
|       username: nacos | ||||
|       password: L4s6f9y3 | ||||
|       server-addr: 49.235.96.75:8848 | ||||
|       ip: 192.168.1.100 | ||||
|       discovery: | ||||
|         server-addr: ${spring.cloud.nacos.server-addr} | ||||
|         username: ${spring.cloud.nacos.username} | ||||
|         password: ${spring.cloud.nacos.password} | ||||
|         ip: ${spring.cloud.nacos.ip} | ||||
| @ -0,0 +1,10 @@ | ||||
| spring: | ||||
|   cloud: | ||||
|     nacos: | ||||
|       username: nacos | ||||
|       password: L4s6f9y3 | ||||
|       server-addr: 49.235.96.75:9001 | ||||
|       discovery: | ||||
|         server-addr: ${spring.cloud.nacos.server-addr} | ||||
|         username: ${spring.cloud.nacos.username} | ||||
|         password: ${spring.cloud.nacos.password} | ||||
| @ -0,0 +1,13 @@ | ||||
| package org.jcnc.llmx.app.multimodality | ||||
| 
 | ||||
| import org.junit.jupiter.api.Test | ||||
| import org.springframework.boot.test.context.SpringBootTest | ||||
| 
 | ||||
| @SpringBootTest | ||||
| class LlmxAppMultimodalityApplicationTests { | ||||
| 
 | ||||
| 	@Test | ||||
| 	fun contextLoads() { | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -2,7 +2,7 @@ dependencies { | ||||
| 
 | ||||
| } | ||||
| /** | ||||
|  * 由于 Kotlin 插件被引入时会自动添加依赖,但根项目不需要 Kotlin 依赖,因此需要排除 Kotlin 依赖 | ||||
|  * 由于 Kotlin 插件引入时会自动添加依赖,但根项目不需要 Kotlin 依赖,因此需要排除 Kotlin 依赖 | ||||
|  */ | ||||
| configurations.implementation { | ||||
|     exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib") | ||||
|  | ||||
							
								
								
									
										3
									
								
								llmx-core/llmx-core-api/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								llmx-core/llmx-core-api/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| /gradlew text eol=lf | ||||
| *.bat text eol=crlf | ||||
| *.jar binary | ||||
							
								
								
									
										40
									
								
								llmx-core/llmx-core-api/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								llmx-core/llmx-core-api/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| HELP.md | ||||
| .gradle | ||||
| build/ | ||||
| !gradle/wrapper/gradle-wrapper.jar | ||||
| !**/src/main/**/build/ | ||||
| !**/src/test/**/build/ | ||||
| 
 | ||||
| ### STS ### | ||||
| .apt_generated | ||||
| .classpath | ||||
| .factorypath | ||||
| .project | ||||
| .settings | ||||
| .springBeans | ||||
| .sts4-cache | ||||
| bin/ | ||||
| !**/src/main/**/bin/ | ||||
| !**/src/test/**/bin/ | ||||
| 
 | ||||
| ### IntelliJ IDEA ### | ||||
| .idea | ||||
| *.iws | ||||
| *.iml | ||||
| *.ipr | ||||
| out/ | ||||
| !**/src/main/**/out/ | ||||
| !**/src/test/**/out/ | ||||
| 
 | ||||
| ### NetBeans ### | ||||
| /nbproject/private/ | ||||
| /nbbuild/ | ||||
| /dist/ | ||||
| /nbdist/ | ||||
| /.nb-gradle/ | ||||
| 
 | ||||
| ### VS Code ### | ||||
| .vscode/ | ||||
| 
 | ||||
| ### Kotlin ### | ||||
| .kotlin | ||||
							
								
								
									
										10
									
								
								llmx-core/llmx-core-api/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								llmx-core/llmx-core-api/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| ext{ | ||||
|     set(ProjectFlags.USE_SPRING_CLOUD_BOM,true) | ||||
| } | ||||
| dependencies { | ||||
|     api(project(Modules.Core.COMMON)) | ||||
|     api(libs.org.reactivestreams.reactiveStreams) | ||||
|     // Spring Cloud OpenFeign | ||||
|     implementation(libs.springCloudStarter.openfeign) | ||||
| } | ||||
| 
 | ||||
| @ -0,0 +1,14 @@ | ||||
| package org.jcnc.llmx.core.api | ||||
| 
 | ||||
| import org.springframework.cloud.openfeign.EnableFeignClients | ||||
| import org.springframework.context.annotation.Configuration | ||||
| 
 | ||||
| /** | ||||
|  *LLM 核心Feign自动配置 | ||||
|  * | ||||
|  * @since 2025-05-08 16:48:23 | ||||
|  * @author gewuyou | ||||
|  */ | ||||
| @Configuration | ||||
| @EnableFeignClients(basePackages = ["org.jcnc.llmx.core.api"]) | ||||
| open class LLMCoreFeignAutoConfiguration | ||||
| @ -0,0 +1,21 @@ | ||||
| package org.jcnc.llmx.core.api | ||||
| 
 | ||||
| import org.jcnc.llmx.core.common.entities.request.ChatRequest | ||||
| import org.jcnc.llmx.core.common.entities.response.ChatResponsePart | ||||
| import org.reactivestreams.Publisher | ||||
| import org.springframework.cloud.openfeign.FeignClient | ||||
| import org.springframework.http.MediaType | ||||
| import org.springframework.web.bind.annotation.PostMapping | ||||
| import org.springframework.web.bind.annotation.RequestBody | ||||
| 
 | ||||
| /** | ||||
|  *LLM 核心客户端 | ||||
|  * | ||||
|  * @since 2025-05-08 16:22:22 | ||||
|  * @author gewuyou | ||||
|  */ | ||||
| @FeignClient(name = "llmx-core-service") // name 对应 Nacos 中注册的服务名 | ||||
| fun interface LLMCoreFeignClient { | ||||
|     @PostMapping("/api/v1/chat/stream", consumes = [MediaType.APPLICATION_JSON_VALUE]) | ||||
|     fun chat(@RequestBody request: ChatRequest): Publisher<ChatResponsePart> | ||||
| } | ||||
| @ -0,0 +1 @@ | ||||
| org.jcnc.llmx.core.api.LLMCoreFeignAutoConfiguration | ||||
							
								
								
									
										3
									
								
								llmx-core/llmx-core-common/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								llmx-core/llmx-core-common/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| /gradlew text eol=lf | ||||
| *.bat text eol=crlf | ||||
| *.jar binary | ||||
							
								
								
									
										40
									
								
								llmx-core/llmx-core-common/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								llmx-core/llmx-core-common/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| HELP.md | ||||
| .gradle | ||||
| build/ | ||||
| !gradle/wrapper/gradle-wrapper.jar | ||||
| !**/src/main/**/build/ | ||||
| !**/src/test/**/build/ | ||||
| 
 | ||||
| ### STS ### | ||||
| .apt_generated | ||||
| .classpath | ||||
| .factorypath | ||||
| .project | ||||
| .settings | ||||
| .springBeans | ||||
| .sts4-cache | ||||
| bin/ | ||||
| !**/src/main/**/bin/ | ||||
| !**/src/test/**/bin/ | ||||
| 
 | ||||
| ### IntelliJ IDEA ### | ||||
| .idea | ||||
| *.iws | ||||
| *.iml | ||||
| *.ipr | ||||
| out/ | ||||
| !**/src/main/**/out/ | ||||
| !**/src/test/**/out/ | ||||
| 
 | ||||
| ### NetBeans ### | ||||
| /nbproject/private/ | ||||
| /nbbuild/ | ||||
| /dist/ | ||||
| /nbdist/ | ||||
| /.nb-gradle/ | ||||
| 
 | ||||
| ### VS Code ### | ||||
| .vscode/ | ||||
| 
 | ||||
| ### Kotlin ### | ||||
| .kotlin | ||||
							
								
								
									
										7
									
								
								llmx-core/llmx-core-common/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								llmx-core/llmx-core-common/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| ext{ | ||||
|     set(ProjectFlags.USE_SPRING_BOOT_BOM,true) | ||||
| } | ||||
| dependencies { | ||||
|     implementation(libs.jackson.core) | ||||
|     implementation(libs.jackson.module.kotlin) | ||||
| } | ||||
| @ -0,0 +1,42 @@ | ||||
| package org.jcnc.llmx.core.common.deserializer | ||||
| 
 | ||||
| import com.fasterxml.jackson.core.JsonParser | ||||
| import com.fasterxml.jackson.databind.DeserializationContext | ||||
| import com.fasterxml.jackson.databind.JsonDeserializer | ||||
| import com.fasterxml.jackson.databind.node.ObjectNode | ||||
| import org.jcnc.llmx.core.common.entities.request.MultiModalContent | ||||
| 
 | ||||
| /** | ||||
|  * 多模态内容反序列化器 | ||||
|  * | ||||
|  * 该类用于将JSON表示的多模态内容反序列化为MultiModalContent数据结构 | ||||
|  * 主要处理两种情况:文本内容和图像内容,并相应地创建Text或Image实例 | ||||
|  * 如果JSON中不包含已知的内容类型,则抛出IllegalArgumentException | ||||
|  * | ||||
|  * @since 2025-05-08 20:41:15 | ||||
|  * @author gewuyou | ||||
|  */ | ||||
| class MultiModalContentDeserializer : JsonDeserializer<MultiModalContent>() { | ||||
|     /** | ||||
|      * 反序列化方法 | ||||
|      * | ||||
|      * 根据JSON节点中的内容类型,创建相应的MultiModalContent实例 | ||||
|      * 如果内容类型未知,则抛出异常 | ||||
|      * | ||||
|      * @param p JsonParser对象,用于解析JSON输入 | ||||
|      * @param ctxt DeserializationContext对象,提供反序列化上下文 | ||||
|      * @return MultiModalContent实例,表示反序列化的多模态内容 | ||||
|      * @throws IllegalArgumentException 如果JSON节点中的内容类型未知 | ||||
|      */ | ||||
|     override fun deserialize(p: JsonParser, ctxt: DeserializationContext): MultiModalContent { | ||||
|         // 解析JSON输入并将其读取为ObjectNode | ||||
|         val node: ObjectNode = p.codec.readTree(p) | ||||
| 
 | ||||
|         // 根据JSON节点中的内容类型,返回相应的MultiModalContent实例 | ||||
|         return when { | ||||
|             node.has("text") -> MultiModalContent.Text(node["text"].asText()) | ||||
|             node.has("image") -> MultiModalContent.Image(node["image"].asText()) | ||||
|             else -> throw IllegalArgumentException("Unknown content type in: $node") | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,52 @@ | ||||
| package org.jcnc.llmx.core.common.entities.request | ||||
| 
 | ||||
| import com.fasterxml.jackson.databind.annotation.JsonDeserialize | ||||
| import org.jcnc.llmx.core.common.deserializer.MultiModalContentDeserializer | ||||
| 
 | ||||
| /** | ||||
|  * MultiModalContent密封类,用于定义消息内容的多种模态 | ||||
|  * 这是一个密封类,意味着所有的子类都需要在这个文件中定义 | ||||
|  */ | ||||
| @JsonDeserialize(using = MultiModalContentDeserializer::class) | ||||
| sealed class MultiModalContent { | ||||
|     /** | ||||
|      * Text数据类,表示文本内容的模态 | ||||
|      * @param text 具体的文本内容 | ||||
|      */ | ||||
|     data class Text(val text: String) : MultiModalContent() | ||||
| 
 | ||||
|     /** | ||||
|      * Image数据类,表示图像内容的模态 | ||||
|      * @param image 图像的base64 | ||||
|      */ | ||||
|     data class Image(val image: String) : MultiModalContent() | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * ChatRequest数据类,用于封装聊天请求的参数 | ||||
|  * @since 2025-04-25 17:07:02 | ||||
|  * @author gewuyou | ||||
|  * @param prompt 用户的聊天提示或消息,是聊天请求的主要输入 | ||||
|  * @param model 使用的聊天模型名称,决定了解析和响应的方式 | ||||
|  * @param messages 包含多个模态消息的列表,每个消息有特定的角色和内容 | ||||
|  * @param options 可选的额外参数集合,用于定制聊天请求的行为和输出 | ||||
|  *                可以包括如最大回复长度、温度(随机性)等 | ||||
|  */ | ||||
| data class ChatRequest( | ||||
|     val prompt: String? = "", | ||||
|     val model: String, | ||||
|     val messages: List<MultiModalMessage> =listOf(), | ||||
|     val options: Map<String, String> = mapOf() | ||||
| ) | ||||
| 
 | ||||
| /** | ||||
|  * MultiModalMessage数据类,用于定义聊天请求中的单个消息 | ||||
|  * @param role 消息的角色,可以是"system", "user", "assistant"等 | ||||
|  * @param content 消息的内容,可以是文本、图像等多种模态的组合 | ||||
|  */ | ||||
| data class MultiModalMessage( | ||||
|     val role: String, // "system", "user", "assistant" | ||||
|     val content: List<MultiModalContent> | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| @ -1,4 +1,4 @@ | ||||
| package org.jcnc.llmx.core.spi.entities.request | ||||
| package org.jcnc.llmx.core.common.entities.request | ||||
| 
 | ||||
| /** | ||||
|  * 嵌入请求类 | ||||
| @ -1,4 +1,4 @@ | ||||
| package org.jcnc.llmx.core.spi.entities.response | ||||
| package org.jcnc.llmx.core.common.entities.response | ||||
| 
 | ||||
| /** | ||||
|  * 聊天响应类 | ||||
| @ -1,4 +1,4 @@ | ||||
| package org.jcnc.llmx.core.spi.entities.response | ||||
| package org.jcnc.llmx.core.common.entities.response | ||||
| 
 | ||||
| /** | ||||
|  * 嵌入响应类 | ||||
| @ -1,10 +1,12 @@ | ||||
| package org.jcnc.llmx.core.service.controller | ||||
| 
 | ||||
| import com.gewuyou.forgeboot.core.extension.log | ||||
| import com.gewuyou.forgeboot.webmvc.version.annotation.ApiVersion | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import org.jcnc.llmx.core.common.entities.request.ChatRequest | ||||
| import org.jcnc.llmx.core.common.entities.response.ChatResponsePart | ||||
| import org.jcnc.llmx.core.service.service.impl.LLMServiceImpl | ||||
| import org.jcnc.llmx.core.spi.entities.request.ChatRequest | ||||
| import org.jcnc.llmx.core.spi.entities.response.ChatResponsePart | ||||
| 
 | ||||
| import org.reactivestreams.Publisher | ||||
| import org.springframework.http.MediaType | ||||
| 
 | ||||
| import org.springframework.web.bind.annotation.PostMapping | ||||
| @ -35,7 +37,22 @@ class ChatController( | ||||
|      * @return 返回一个Flow流,流中依次提供了聊天响应的部分数据,允许异步处理和逐步消费响应内容 | ||||
|      */ | ||||
|     @PostMapping("/stream", produces = [MediaType.APPLICATION_NDJSON_VALUE]) | ||||
|     fun chat(@RequestBody request: ChatRequest): Flow<ChatResponsePart> { | ||||
|     fun chat(@RequestBody request: ChatRequest): Publisher<ChatResponsePart> { | ||||
|         return llmServiceImpl.chat(request) | ||||
|     } | ||||
|     /** | ||||
|      * 处理多模态聊天请求的函数 | ||||
|      * 该函数通过 POST 方法接收一个聊天请求,并以 NDJSON 的形式返回聊天响应的部分 | ||||
|      * | ||||
|      * @param request 包含聊天请求信息的数据类 | ||||
|      * @return 返回一个发布者,用于异步地发送聊天响应的部分 | ||||
|      * | ||||
|      * 注意:该函数被设计为异步处理,以提高性能和响应性 | ||||
|      * 它使用了响应式编程模型,适合处理高并发和大数据量的响应 | ||||
|      */ | ||||
|     @PostMapping("/streamMultimodality", produces = [MediaType.APPLICATION_NDJSON_VALUE]) | ||||
|     fun multimodalityChat(@RequestBody request: ChatRequest): Publisher<ChatResponsePart>{ | ||||
|         log.info("request: {}", request) | ||||
|         return llmServiceImpl.multimodalityChat(request) | ||||
|     } | ||||
| } | ||||
| @ -1,8 +1,9 @@ | ||||
| package org.jcnc.llmx.core.service.service | ||||
| 
 | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import org.jcnc.llmx.core.spi.entities.request.ChatRequest | ||||
| import org.jcnc.llmx.core.spi.entities.response.ChatResponsePart | ||||
| 
 | ||||
| import org.jcnc.llmx.core.common.entities.request.ChatRequest | ||||
| import org.jcnc.llmx.core.common.entities.response.ChatResponsePart | ||||
| import org.reactivestreams.Publisher | ||||
| 
 | ||||
| import org.springframework.web.bind.annotation.PostMapping | ||||
| 
 | ||||
| @ -12,7 +13,7 @@ import org.springframework.web.bind.annotation.PostMapping | ||||
|  * @since 2025-04-26 17:38:18 | ||||
|  * @author gewuyou | ||||
|  */ | ||||
| fun interface LLMService { | ||||
| interface LLMService { | ||||
|     /** | ||||
|      * 初始化与聊天服务的连接,以处理聊天请求 | ||||
|      * | ||||
| @ -23,5 +24,6 @@ fun interface LLMService { | ||||
|      * @return 返回一个Flow流,通过该流可以接收到聊天响应的部分数据,如消息、状态更新等 | ||||
|      */ | ||||
|     @PostMapping("/chat") | ||||
|     fun chat(request: ChatRequest): Flow<ChatResponsePart> | ||||
|     fun chat(request: ChatRequest): Publisher<ChatResponsePart> | ||||
|     fun multimodalityChat(request: ChatRequest): Publisher<ChatResponsePart> | ||||
| } | ||||
| @ -1,11 +1,11 @@ | ||||
| package org.jcnc.llmx.core.service.service.impl | ||||
| 
 | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.reactive.asFlow | ||||
| 
 | ||||
| import org.jcnc.llmx.core.common.entities.request.ChatRequest | ||||
| import org.jcnc.llmx.core.common.entities.response.ChatResponsePart | ||||
| import org.jcnc.llmx.core.service.manager.ModelRouteManager | ||||
| import org.jcnc.llmx.core.service.service.LLMService | ||||
| import org.jcnc.llmx.core.spi.entities.request.ChatRequest | ||||
| import org.jcnc.llmx.core.spi.entities.response.ChatResponsePart | ||||
| import org.reactivestreams.Publisher | ||||
| 
 | ||||
| import org.springframework.stereotype.Service | ||||
| import org.springframework.web.reactive.function.client.WebClient | ||||
| @ -30,7 +30,7 @@ class LLMServiceImpl( | ||||
|      * @param request 聊天请求对象,包含建立聊天所需的信息,如用户标识、会话标识等 | ||||
|      * @return 返回一个Flow流,通过该流可以接收到聊天响应的部分数据,如消息、状态更新等 | ||||
|      */ | ||||
|     override fun chat(request: ChatRequest): Flow<ChatResponsePart> { | ||||
|     override fun chat(request: ChatRequest): Publisher<ChatResponsePart> { | ||||
|         val serviceName = modelRouteManager.resolveServiceName(request.model) | ||||
|         val webClient = webClientBuilder.build() | ||||
|         return webClient.post() | ||||
| @ -38,6 +38,15 @@ class LLMServiceImpl( | ||||
|             .bodyValue(request) | ||||
|             .retrieve() | ||||
|             .bodyToFlux(ChatResponsePart::class.java) | ||||
|             .asFlow() | ||||
|     } | ||||
| 
 | ||||
|     override fun multimodalityChat(request: ChatRequest): Publisher<ChatResponsePart> { | ||||
|         val serviceName = modelRouteManager.resolveServiceName(request.model) | ||||
|         val webClient = webClientBuilder.build() | ||||
|         return webClient.post() | ||||
|             .uri("http://$serviceName/provider/multimodalityChat") | ||||
|             .bodyValue(request) | ||||
|             .retrieve() | ||||
|             .bodyToFlux(ChatResponsePart::class.java) | ||||
|     } | ||||
| } | ||||
| @ -9,5 +9,6 @@ llmx: | ||||
|       qwen-turbo: llmx-impl-baiLian | ||||
|       qwen-max: llmx-impl-baiLian | ||||
|       qwen-plus: llmx-impl-baiLian | ||||
|       qwen-vl-max-latest: llmx-impl-baiLian | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -0,0 +1,7 @@ | ||||
| server: | ||||
|   port: 9002 | ||||
| spring: | ||||
|   cloud: | ||||
|     nacos: | ||||
|       discovery: | ||||
|         server-addr: 49.235.96.75:9001 | ||||
| @ -9,5 +9,6 @@ llmx: | ||||
|       qwen-turbo: llmx-impl-baiLian | ||||
|       qwen-max: llmx-impl-baiLian | ||||
|       qwen-plus: llmx-impl-baiLian | ||||
|       qwen-vl-max-latest: llmx-impl-baiLian | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -4,6 +4,7 @@ apply { | ||||
|     plugin(libs.plugins.kotlin.plugin.spring.get().pluginId) | ||||
| } | ||||
| dependencies { | ||||
|     compileOnly(libs.kotlinx.coruntes.reactor) | ||||
|     compileOnly(libs.springBootStarter.web) | ||||
|     api(libs.org.reactivestreams.reactiveStreams) | ||||
|     api(project(Modules.Core.COMMON)) | ||||
| } | ||||
| @ -1,17 +0,0 @@ | ||||
| package org.jcnc.llmx.core.spi.entities.request | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * ChatRequest数据类,用于封装聊天请求的参数 | ||||
|  * @since 2025-04-25 17:07:02 | ||||
|  * @author gewuyou | ||||
|  * @param prompt 用户的聊天提示或消息,是聊天请求的主要输入 | ||||
|  * @param model 使用的聊天模型名称,决定了解析和响应的方式 | ||||
|  * @param options 可选的额外参数集合,用于定制聊天请求的行为和输出 | ||||
|  *                可以包括如最大回复长度、温度(随机性)等 | ||||
|  */ | ||||
| data class ChatRequest( | ||||
|     val prompt: String, | ||||
|     val model: String, | ||||
|     val options: Map<String, String> = mapOf() | ||||
| ) | ||||
| @ -1,9 +1,10 @@ | ||||
| package org.jcnc.llmx.core.spi.provider | ||||
| 
 | ||||
| import org.jcnc.llmx.core.spi.entities.request.ChatRequest | ||||
| import org.jcnc.llmx.core.spi.entities.request.EmbeddingRequest | ||||
| import org.jcnc.llmx.core.spi.entities.response.ChatResponsePart | ||||
| import org.jcnc.llmx.core.spi.entities.response.EmbeddingResponse | ||||
| 
 | ||||
| import org.jcnc.llmx.core.common.entities.request.ChatRequest | ||||
| import org.jcnc.llmx.core.common.entities.request.EmbeddingRequest | ||||
| import org.jcnc.llmx.core.common.entities.response.ChatResponsePart | ||||
| import org.jcnc.llmx.core.common.entities.response.EmbeddingResponse | ||||
| import org.reactivestreams.Publisher | ||||
| import org.springframework.http.MediaType | ||||
| import org.springframework.web.bind.annotation.PostMapping | ||||
| @ -31,7 +32,18 @@ interface LLMProvider { | ||||
|      */ | ||||
|     @PostMapping("/chat", produces = [MediaType.APPLICATION_NDJSON_VALUE]) | ||||
|     fun chat(request: ChatRequest): Publisher<ChatResponsePart> | ||||
| 
 | ||||
|     /** | ||||
|      * 处理多模态聊天请求的函数 | ||||
|      * 该函数通过 POST 方法接收一个聊天请求,并以 NDJSON 的形式返回聊天响应的部分 | ||||
|      * | ||||
|      * @param request 包含聊天请求信息的数据类 | ||||
|      * @return 返回一个发布者,用于异步地发送聊天响应的部分 | ||||
|      * | ||||
|      * 注意:该函数被设计为异步处理,以提高性能和响应性 | ||||
|      * 它使用了响应式编程模型,适合处理高并发和大数据量的响应 | ||||
|      */ | ||||
|     @PostMapping("/multimodalityChat", produces = [MediaType.APPLICATION_NDJSON_VALUE]) | ||||
|     fun multimodalityChat(request: ChatRequest): Publisher<ChatResponsePart> | ||||
|     /** | ||||
|      * 嵌入功能方法 | ||||
|      * 该方法允许用户发送嵌入请求,以获取LLM生成的嵌入向量 | ||||
|  | ||||
| @ -11,7 +11,8 @@ import okhttp3.MediaType.Companion.toMediaType | ||||
| import okhttp3.OkHttpClient | ||||
| import okhttp3.Request | ||||
| import okhttp3.RequestBody.Companion.toRequestBody | ||||
| import org.jcnc.llmx.core.spi.entities.response.ChatResponsePart | ||||
| import org.jcnc.llmx.core.common.entities.response.ChatResponsePart | ||||
| 
 | ||||
| 
 | ||||
| import org.springframework.stereotype.Component | ||||
| import java.io.BufferedReader | ||||
| @ -30,7 +31,7 @@ import java.io.BufferedReader | ||||
| @Component | ||||
| class DashScopeAdapter( | ||||
|     private val okHttpClient: OkHttpClient, | ||||
|     private val objectMapper: ObjectMapper | ||||
|     private val objectMapper: ObjectMapper, | ||||
| ) { | ||||
|     /** | ||||
|      * 发送流式聊天请求 | ||||
| @ -50,7 +51,7 @@ class DashScopeAdapter( | ||||
|         headers: Map<String, String>, | ||||
|         requestBody: Any, | ||||
|         extractContent: (String) -> ChatResponsePart, | ||||
|         dispatcher: CoroutineDispatcher = Dispatchers.IO | ||||
|         dispatcher: CoroutineDispatcher = Dispatchers.IO, | ||||
|     ): Flow<ChatResponsePart> = flow { | ||||
|         val requestJson = objectMapper.writeValueAsString(requestBody) | ||||
|         log.info("📤 请求参数: {}", requestJson) | ||||
| @ -59,6 +60,7 @@ class DashScopeAdapter( | ||||
| 
 | ||||
|         okHttpClient.newCall(request).execute().use { response -> | ||||
|             if (!response.isSuccessful) { | ||||
|                 log.error("🚨 DashScope 请求失败: code ${response.code} message: ${response.message} body: ${response.body}") | ||||
|                 throw RuntimeException("❌ DashScope 请求失败: HTTP ${response.code}") | ||||
|             } | ||||
| 
 | ||||
| @ -92,7 +94,7 @@ class DashScopeAdapter( | ||||
|         dispatcher: CoroutineDispatcher, | ||||
|         reader: BufferedReader, | ||||
|         extractContent: (String) -> ChatResponsePart, | ||||
|         allContent: StringBuilder | ||||
|         allContent: StringBuilder, | ||||
|     ) { | ||||
|         while (currentCoroutineContext().isActive) { | ||||
|             val line = withContext(dispatcher) { | ||||
|  | ||||
| @ -44,5 +44,9 @@ class DashScopeProperties { | ||||
|      * 应用调用基路径,例如 /api/v1。 | ||||
|      */ | ||||
|     var baseUrl: String = "" | ||||
|     /** | ||||
|      * 多模态对话调用路径 | ||||
|      */ | ||||
|     var multimodalityUrl: String = "" | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -2,10 +2,11 @@ package org.jcnc.llmx.impl.baiLian.controller | ||||
| 
 | ||||
| 
 | ||||
| import kotlinx.coroutines.reactive.asPublisher | ||||
| import org.jcnc.llmx.core.spi.entities.request.ChatRequest | ||||
| import org.jcnc.llmx.core.spi.entities.request.EmbeddingRequest | ||||
| import org.jcnc.llmx.core.spi.entities.response.ChatResponsePart | ||||
| import org.jcnc.llmx.core.spi.entities.response.EmbeddingResponse | ||||
| import org.jcnc.llmx.core.common.entities.request.ChatRequest | ||||
| import org.jcnc.llmx.core.common.entities.request.EmbeddingRequest | ||||
| import org.jcnc.llmx.core.common.entities.response.ChatResponsePart | ||||
| import org.jcnc.llmx.core.common.entities.response.EmbeddingResponse | ||||
| 
 | ||||
| import org.jcnc.llmx.core.spi.provider.LLMProvider | ||||
| import org.jcnc.llmx.impl.baiLian.service.BaiLianModelService | ||||
| import org.reactivestreams.Publisher | ||||
| @ -37,6 +38,10 @@ class BaiLianProvider( | ||||
|         return baiLianModelService.streamChat(request).asPublisher() | ||||
|     } | ||||
| 
 | ||||
|     override fun multimodalityChat(@RequestBody request: ChatRequest): Publisher<ChatResponsePart> { | ||||
|         return baiLianModelService.streamMultimodalityChat(request).asPublisher() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 嵌入功能方法 | ||||
|      * 该方法允许用户发送嵌入请求,以获取LLM生成的嵌入向量 | ||||
|  | ||||
| @ -0,0 +1,117 @@ | ||||
| package org.jcnc.llmx.impl.baiLian.entities.response | ||||
| 
 | ||||
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties | ||||
| import com.fasterxml.jackson.annotation.JsonProperty | ||||
| 
 | ||||
| /** | ||||
|  * Dash范围响应类 | ||||
|  * | ||||
|  * 该类用于表示从Dash服务接收到的响应数据结构它包含了响应的输出信息、使用情况和请求ID | ||||
|  * 主要用于JSON序列化和反序列化 | ||||
|  * | ||||
|  * @param output 输出信息,包括生成的内容和完成原因 | ||||
|  * @param usage 使用情况,包括输入和输出的令牌详情 | ||||
|  * @param requestId 请求ID,用于跟踪和调试请求 | ||||
|  */ | ||||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||||
| data class DashScopeResponse( | ||||
|     val output: Output?, | ||||
|     val usage: Usage?, | ||||
|     @JsonProperty("request_id") | ||||
|     val requestId: String? | ||||
| ) | ||||
| 
 | ||||
| /** | ||||
|  * 输出数据类 | ||||
|  * | ||||
|  * 该类包含Dash服务生成的内容,主要是通过choices列表来提供可能的回复选项 | ||||
|  * | ||||
|  * @param choices 生成内容的选项列表 | ||||
|  */ | ||||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||||
| data class Output( | ||||
|     val choices: List<Choice>? | ||||
| ) | ||||
| 
 | ||||
| /** | ||||
|  * 选择项数据类 | ||||
|  * | ||||
|  * 该类表示生成内容的一个选项,包括完成原因和具体的消息内容 | ||||
|  * | ||||
|  * @param finishReason 完成原因,解释生成内容结束的原因 | ||||
|  * @param message 消息内容,包括角色和具体内容 | ||||
|  */ | ||||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||||
| data class Choice( | ||||
|     @JsonProperty("finish_reason") | ||||
|     val finishReason: String?, | ||||
|     val message: Message? | ||||
| ) | ||||
| 
 | ||||
| /** | ||||
|  * 消息数据类 | ||||
|  * | ||||
|  * 该类表示一条消息,包括消息的角色(如用户、助手)和具体内容 | ||||
|  * 具体内容通过一个内容列表来表示 | ||||
|  * | ||||
|  * @param role 消息的角色,如用户、助手等 | ||||
|  * @param content 消息的具体内容列表 | ||||
|  */ | ||||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||||
| data class Message( | ||||
|     val role: String?, | ||||
|     val content: List<Content>? | ||||
| ) | ||||
| 
 | ||||
| /** | ||||
|  * 内容数据类 | ||||
|  * | ||||
|  * 该类表示消息中的具体内容,目前只支持文本内容 | ||||
|  * | ||||
|  * @param text 文本内容 | ||||
|  */ | ||||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||||
| data class Content( | ||||
|     val text: String? | ||||
| ) | ||||
| 
 | ||||
| /** | ||||
|  * 使用情况数据类 | ||||
|  * | ||||
|  * 该类详细记录了Dash服务的使用情况,包括输入和输出的令牌数量和详情 | ||||
|  * | ||||
|  * @param inputTokensDetails 输入令牌的详情 | ||||
|  * @param outputTokensDetails 输出令牌的详情 | ||||
|  * @param inputTokens 输入令牌的数量 | ||||
|  * @param outputTokens 输出令牌的数量 | ||||
|  * @param totalTokens 总令牌的数量 | ||||
|  * @param imageTokens 图像令牌的数量,如果适用 | ||||
|  */ | ||||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||||
| data class Usage( | ||||
|     @JsonProperty("input_tokens_details") | ||||
|     val inputTokensDetails: TokenDetail?, | ||||
|     @JsonProperty("output_tokens_details") | ||||
|     val outputTokensDetails: TokenDetail?, | ||||
|     @JsonProperty("input_tokens") | ||||
|     val inputTokens: Int?, | ||||
|     @JsonProperty("output_tokens") | ||||
|     val outputTokens: Int?, | ||||
|     @JsonProperty("total_tokens") | ||||
|     val totalTokens: Int?, | ||||
|     @JsonProperty("image_tokens") | ||||
|     val imageTokens: Int? | ||||
| ) | ||||
| 
 | ||||
| /** | ||||
|  * 令牌详情数据类 | ||||
|  * | ||||
|  * 该类提供了令牌的详细信息,目前仅支持文本令牌的数量 | ||||
|  * | ||||
|  * @param textTokens 文本令牌的数量 | ||||
|  */ | ||||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||||
| data class TokenDetail( | ||||
|     @JsonProperty("text_tokens") | ||||
|     val textTokens: Int? | ||||
| ) | ||||
| @ -1,8 +1,8 @@ | ||||
| package org.jcnc.llmx.impl.baiLian.service | ||||
| 
 | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import org.jcnc.llmx.core.spi.entities.request.ChatRequest | ||||
| import org.jcnc.llmx.core.spi.entities.response.ChatResponsePart | ||||
| import org.jcnc.llmx.core.common.entities.request.ChatRequest | ||||
| import org.jcnc.llmx.core.common.entities.response.ChatResponsePart | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
| @ -20,4 +20,11 @@ interface BaiLianModelService { | ||||
|      * @return 返回一个Flow流,包含部分聊天响应,允许逐步处理和消费响应 | ||||
|      */ | ||||
|     fun streamChat(request: ChatRequest): Flow<ChatResponsePart> | ||||
|     /** | ||||
|      * 使用流式多模态聊天交互 | ||||
|      * | ||||
|      * @param request 聊天请求对象,包含用户输入、上下文等信息 | ||||
|      * @return 返回一个Flow流,包含部分聊天响应,允许逐步处理和消费响应 | ||||
|      */ | ||||
|     fun streamMultimodalityChat(request: ChatRequest) : Flow<ChatResponsePart> | ||||
| } | ||||
|  | ||||
| @ -4,11 +4,13 @@ import com.fasterxml.jackson.core.type.TypeReference | ||||
| import com.fasterxml.jackson.databind.ObjectMapper | ||||
| import com.gewuyou.forgeboot.core.extension.log | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import org.jcnc.llmx.core.spi.entities.request.ChatRequest | ||||
| import org.jcnc.llmx.core.spi.entities.response.ChatResponsePart | ||||
| import org.jcnc.llmx.core.spi.entities.response.Usage | ||||
| import org.jcnc.llmx.core.common.entities.request.ChatRequest | ||||
| import org.jcnc.llmx.core.common.entities.response.ChatResponsePart | ||||
| import org.jcnc.llmx.core.common.entities.response.Usage | ||||
| 
 | ||||
| import org.jcnc.llmx.impl.baiLian.adapter.DashScopeAdapter | ||||
| import org.jcnc.llmx.impl.baiLian.config.entities.DashScopeProperties | ||||
| import org.jcnc.llmx.impl.baiLian.entities.response.DashScopeResponse | ||||
| import org.jcnc.llmx.impl.baiLian.service.BaiLianModelService | ||||
| 
 | ||||
| import org.springframework.stereotype.Service | ||||
| @ -123,4 +125,75 @@ class BaiLianModelServiceImpl( | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 使用流式多模态聊天交互 | ||||
|      * | ||||
|      * @param request 聊天请求对象,包含用户输入、上下文等信息 | ||||
|      * @return 返回一个Flow流,包含部分聊天响应,允许逐步处理和消费响应 | ||||
|      */ | ||||
|     override fun streamMultimodalityChat(request: ChatRequest): Flow<ChatResponsePart> { | ||||
|         // 构造请求URL | ||||
|         val url = dashScopeProperties.multimodalityUrl | ||||
|         log.info("请求URL: $url") | ||||
|         // 构造请求头,包括授权信息和内容类型 | ||||
|         val headers = mapOf( | ||||
|             "Authorization" to "Bearer ${dashScopeProperties.apiKey}", | ||||
|             "Content-Type" to "application/json", | ||||
|             "X-DashScope-SSE" to "enable" | ||||
|         ) | ||||
|         log.info("请求头: $headers") | ||||
|         // 构造输入参数,主要包括用户的message | ||||
|         val inputMap = mutableMapOf("messages" to request.messages) | ||||
|         // 构造参数地图,包括是否包含思考、模型ID、增量输出等 | ||||
|         val parametersMap = mutableMapOf( | ||||
|             "incremental_output" to true, | ||||
|         ) | ||||
|         // 构造请求体,包括输入参数、构造的参数地图和调试信息 | ||||
|         val body = mapOf( | ||||
|             "model" to request.model, | ||||
|             "input" to inputMap, | ||||
|             "parameters" to parametersMap, | ||||
|         ) | ||||
|         // 发送流式聊天请求,并处理响应 | ||||
|         return dashScopeAdapter.sendStreamChat( | ||||
|             url, headers, body, | ||||
|             { json: String -> | ||||
|                 val response = objectMapper.readValue(json, DashScopeResponse::class.java) | ||||
|                 val choices = response | ||||
|                     .output | ||||
|                     ?.choices | ||||
|                     ?.getOrNull(0) | ||||
|                 // 提取输出文本 | ||||
|                 val text = choices | ||||
|                     ?.message | ||||
|                     ?.content | ||||
|                     ?.getOrNull(0) | ||||
|                     ?.text | ||||
|                     ?: "" | ||||
|                 // 判断是否完成(finish_reason 通常为 "stop",但你这里是字符串 "null") | ||||
|                 val finishReason = choices?.finishReason | ||||
|                 val done = finishReason == "stop" | ||||
|                 // 提取使用情况 | ||||
|                 val usage = response.usage | ||||
|                 val promptTokens = usage?.inputTokens ?: 0 | ||||
|                 val completionTokens = usage?.outputTokens ?: 0 | ||||
|                 val totalTokens = usage?.totalTokens ?: 0 | ||||
|                 ChatResponsePart( | ||||
|                     content = text, | ||||
|                     done = done, | ||||
|                     usage = Usage( | ||||
|                         promptTokens = promptTokens, | ||||
|                         completionTokens = completionTokens, | ||||
|                         totalTokens = totalTokens | ||||
|                     ), | ||||
|                     other = mapOf( | ||||
|                         "request_id" to (response.requestId ?: ""), | ||||
|                         "model" to request.model, | ||||
|                         "response" to json | ||||
|                     ) | ||||
|                 ) | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -22,3 +22,4 @@ aliyun: | ||||
|       api-key: sk-78af4dd964a94f4cb373851064dbdc12  # API密钥 | ||||
|       app-id: 3fae0bbab2e54a90a37aa02cd12dd62c  # 应用ID | ||||
|       base-url: https://dashscope.aliyuncs.com/api/v1/apps/  # 基础API URL | ||||
|       multimodality-url: https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation | ||||
|  | ||||
| @ -18,3 +18,4 @@ aliyun: | ||||
|       api-key: sk-78af4dd964a94f4cb373851064dbdc12  # API密钥 | ||||
|       app-id: 3fae0bbab2e54a90a37aa02cd12dd62c  # 应用ID | ||||
|       base-url: https://dashscope.aliyuncs.com/api/v1/apps/  # 基础API URL | ||||
|       multimodality-url: https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation | ||||
|  | ||||
| @ -0,0 +1,21 @@ | ||||
| package com.gewuyou.bailian.util; | ||||
| 
 | ||||
| import java.nio.file.Files; | ||||
| import java.nio.file.Paths; | ||||
| import java.util.Base64; | ||||
| 
 | ||||
| /** | ||||
|  * Base 64 图像 | ||||
|  * | ||||
|  * @author gewuyou | ||||
|  * @since 2025-05-08 18:56:02 | ||||
|  */ | ||||
| public class Base64Image { | ||||
|     public static void main(String[] args) throws Exception { | ||||
|         byte[] imageBytes = Files.readAllBytes(Paths.get("F:/gewuyou/Project/Idea/demo-llm/src/main/resources/2025俏春装/10147009251.jpg")); | ||||
|         String base64Image = Base64.getEncoder().encodeToString(imageBytes); | ||||
|         String imagePayload = "data:image/jpeg;base64," + base64Image; | ||||
| 
 | ||||
|         System.out.println(imagePayload); // 你可以把它放到 JSON 中使用 | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,85 @@ | ||||
| package com.gewuyou.bailian.util; | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| import java.io.BufferedReader; | ||||
| import java.io.File; | ||||
| import java.io.InputStreamReader; | ||||
| import java.io.OutputStream; | ||||
| import java.net.HttpURLConnection; | ||||
| import java.net.URL; | ||||
| import java.nio.file.Files; | ||||
| import java.util.Base64; | ||||
| /** | ||||
|  * DashScopeLocalImageClient | ||||
|  * | ||||
|  * @author gewuyou | ||||
|  * @since 2025-05-08 18:57:58 | ||||
|  */ | ||||
| public class DashScopeLocalImageClient { | ||||
| 
 | ||||
|     public static void main(String[] args) throws Exception { | ||||
|         // Step 1: 读取本地图片并转成 base64 | ||||
|         String imagePath = "F:/gewuyou/Project/Idea/demo-llm/src/main/resources/2025俏春装/10147009251.jpg"; | ||||
|         byte[] imageBytes = Files.readAllBytes(new File(imagePath).toPath()); | ||||
|         String base64Image = Base64.getEncoder().encodeToString(imageBytes); | ||||
|         String imageDataUrl = "data:image/jpeg;base64," + base64Image; | ||||
|         System.out.println(imageDataUrl); | ||||
|         // Step 2: 构造请求 JSON | ||||
|         String jsonBody = """ | ||||
|         { | ||||
|           "model": "qwen-vl-max-latest", | ||||
|           "input": { | ||||
|             "messages": [ | ||||
|               { | ||||
|                 "role": "system", | ||||
|                 "content": [ | ||||
|                   { "text": "You are a helpful assistant." } | ||||
|                 ] | ||||
|               }, | ||||
|               { | ||||
|                 "role": "user", | ||||
|                 "content": [ | ||||
|                   { "image": "%s" }, | ||||
|                   { "text": "请给这张图打标签" } | ||||
|                 ] | ||||
|               } | ||||
|             ] | ||||
|           }, | ||||
|            "parameters": { | ||||
|                "incremental_output": true | ||||
|            } | ||||
|         } | ||||
|         """.formatted(imageDataUrl.replace("\n", "")); | ||||
| 
 | ||||
|         // Step 3: 发送 POST 请求 | ||||
|         URL url = new URL("https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"); | ||||
|         HttpURLConnection connection = (HttpURLConnection) url.openConnection(); | ||||
| 
 | ||||
|         connection.setRequestMethod("POST"); | ||||
|         connection.setRequestProperty("Authorization", "Bearer sk-78af4dd964a94f4cb373851064dbdc12"); | ||||
|         connection.setRequestProperty("Content-Type", "application/json"); | ||||
|         connection.setDoOutput(true); | ||||
| 
 | ||||
|         try (OutputStream os = connection.getOutputStream()) { | ||||
|             os.write(jsonBody.getBytes()); | ||||
|             os.flush(); | ||||
|         } | ||||
| 
 | ||||
|         // Step 4: 读取响应 | ||||
|         int responseCode = connection.getResponseCode(); | ||||
|         System.out.println("Response Code: " + responseCode); | ||||
| 
 | ||||
|         try (BufferedReader reader = new BufferedReader( | ||||
|                 new InputStreamReader(responseCode == 200 ? | ||||
|                         connection.getInputStream() : connection.getErrorStream(), "GBK"))) { | ||||
| 
 | ||||
|             String line; | ||||
|             while ((line = reader.readLine()) != null) { | ||||
|                 System.out.println(line); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         connection.disconnect(); | ||||
|     } | ||||
| } | ||||
| @ -6,15 +6,25 @@ include( | ||||
|     "llmx-core", | ||||
|     ":llmx-core:llmx-core-service", | ||||
|     ":llmx-core:llmx-core-spi", | ||||
|     ":llmx-core:llmx-core-api", | ||||
|     ":llmx-core:llmx-core-common", | ||||
| 
 | ||||
|     ) | ||||
| project(":llmx-core:llmx-core-service").name = "llmx-core-service" | ||||
| project(":llmx-core:llmx-core-spi").name = "llmx-core-spi" | ||||
| project(":llmx-core:llmx-core-api").name = "llmx-core-api" | ||||
| project(":llmx-core:llmx-core-common").name = "llmx-core-common" | ||||
| 
 | ||||
| include( | ||||
|     "llmx-impl", | ||||
|     "llmx-impl:llmx-impl-bailian", | ||||
|     ":llmx-impl:llmx-impl-bailian", | ||||
| //    "llmx-impl:llmx-impl-openAi", | ||||
| ) | ||||
| project(":llmx-impl:llmx-impl-bailian").name = "llmx-impl-bailian" | ||||
| //project(":llmx-impl:llmx-impl-openAi").name = "llmx-impl-openAi" | ||||
| //project(":llmx-impl:llmx-impl-openAi").name = "llmx-impl-openAi" | ||||
| 
 | ||||
| include( | ||||
|     "llmx-app", | ||||
|     ":llmx-app:llmx-app-multimodality" | ||||
| ) | ||||
| project(":llmx-app:llmx-app-multimodality").name = "llmx-app-multimodality" | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user