feat(impl): 添加百炼模型服务实现
- 新增 BaiLianModelService 接口及其实现类 BaiLianModelServiceImpl- 添加与阿里云 DashScope API 交互的适配器 DashScopeAdapter - 新增百炼提供商 BaiLianProvider - 更新 ChatController 以支持流式聊天- 添加必要的配置类和属性文件
This commit is contained in:
parent
3c9796524f
commit
05a2a78dfd
@ -4,6 +4,7 @@ plugins {
|
||||
alias(libs.plugins.kotlin.plugin.spring)
|
||||
alias(libs.plugins.spring.boot)
|
||||
alias(libs.plugins.spring.dependency.management)
|
||||
alias(libs.plugins.jibLocalPlugin)
|
||||
}
|
||||
|
||||
group = "org.jcnc"
|
||||
@ -91,7 +92,7 @@ subprojects {
|
||||
// springCloudBom
|
||||
if (project.getPropertyByBoolean(ProjectFlags.USE_SPRING_CLOUD_BOM)) {
|
||||
dependencies {
|
||||
implementation(libs.springCloudDependencies.bom)
|
||||
implementation(platform(libs.springCloudDependencies.bom))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -99,6 +100,7 @@ subprojects {
|
||||
apply {
|
||||
plugin(libs.plugins.java.get().pluginId)
|
||||
plugin(libs.plugins.kotlin.jvm.get().pluginId)
|
||||
plugin(libs.plugins.jibLocalPlugin.get().pluginId)
|
||||
}
|
||||
println(project.name + ":" + project.getPropertyByBoolean(ProjectFlags.USE_SPRING_BOOT))
|
||||
|
||||
@ -107,6 +109,13 @@ subprojects {
|
||||
freeCompilerArgs.addAll("-Xjsr305=strict")
|
||||
}
|
||||
}
|
||||
jibConfig{
|
||||
project{
|
||||
projectName = "llmhub-core-service"
|
||||
ports = listOf("9002")
|
||||
environment = mapOf("SPRING_PROFILES_ACTIVE" to "prod")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
|
||||
@ -6,10 +6,17 @@ plugins {
|
||||
alias(libs.plugins.javaGradle.plugin)
|
||||
}
|
||||
dependencies {
|
||||
// 导入 jib 插件依赖
|
||||
implementation(libs.jib.gradlePlugin)
|
||||
}
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
|
||||
register("jib-plugin") {
|
||||
id = "org.jcnc.llmhub.plugin.jib"
|
||||
implementationClass = "org.jcnc.llmhub.plugin.jib.JibPlugin"
|
||||
description =
|
||||
"提供简单的配置构建镜像"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,83 @@
|
||||
package org.jcnc.llmhub.plugin.jib
|
||||
|
||||
|
||||
import org.jcnc.llmhub.plugin.jib.entity.JibProject
|
||||
import com.google.cloud.tools.jib.gradle.JibExtension
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
|
||||
/**
|
||||
*JIB插件
|
||||
*
|
||||
* @since 2025-03-30 12:11:23
|
||||
* @author gewuyou
|
||||
*/
|
||||
class JibPlugin : Plugin<Project> {
|
||||
override fun apply(project: Project) {
|
||||
// 创建扩展
|
||||
val extension = project.extensions.create("jibConfig", JibPluginExtension::class.java)
|
||||
project.afterEvaluate {
|
||||
// 只有匹配的模块才会实际应用JIB插件
|
||||
if (extension.projects.any { it.projectName == project.name }) {
|
||||
project.plugins.apply("com.google.cloud.tools.jib")
|
||||
// 调用配置逻辑
|
||||
project.configureJib(extension.projects)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 新增扩展类
|
||||
open class JibPluginExtension {
|
||||
val projects = mutableListOf<JibProject>()
|
||||
fun project(configure: JibProject.() -> Unit) {
|
||||
projects.add(JibProject().apply(configure))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*项目扩展
|
||||
*
|
||||
* @since 2025-03-30 01:40:10
|
||||
* @author gewuyou
|
||||
*/
|
||||
private fun Project.configureJib(jibProjects: List<JibProject>) {
|
||||
val jibProject = jibProjects.find { it.projectName == project.name }
|
||||
jibProject?.let {
|
||||
extensions.configure<JibExtension> {
|
||||
from {
|
||||
image = jibProject.baseImage
|
||||
}
|
||||
to {
|
||||
image =
|
||||
"${System.getenv("LUKE_SERVER_DOCKER_REGISTRY_URL")}/${jibProject.imageName}:${jibProject.version}"
|
||||
auth {
|
||||
username = "root"
|
||||
password = System.getenv("LUKE_SERVER_DOCKER_REGISTRY_PASSWORD")
|
||||
}
|
||||
}
|
||||
// 动态配置容器参数
|
||||
container {
|
||||
ports = jibProject.ports
|
||||
environment = jibProject.environment
|
||||
if (jibProject.entrypoint.isNotEmpty()) {
|
||||
entrypoint = jibProject.entrypoint
|
||||
}
|
||||
}
|
||||
|
||||
// 动态配置额外目录
|
||||
extraDirectories {
|
||||
setPaths(jibProject.paths)
|
||||
permissions.putAll(jibProject.permissions)
|
||||
}
|
||||
// 将动态部分移到任务配置中
|
||||
tasks.named("jib").configure {
|
||||
doFirst {
|
||||
// 只有在实际执行jib任务时才会打印日志
|
||||
println("jibProject: $jibProject")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package org.jcnc.llmhub.plugin.jib.entity
|
||||
|
||||
|
||||
/**
|
||||
* Jib 项目配置类
|
||||
* @author gewuyou
|
||||
* @date 2025/03/30
|
||||
* @constructor 创建[JibProject]
|
||||
* @param [projectName] 项目名称
|
||||
* @param [ports] 监听端口
|
||||
* @param [environment] 环境
|
||||
* @param [entrypoint] 入口点(用于自定义启动命令)
|
||||
* @param [imageName] 图像名称
|
||||
* @param [version] 版本
|
||||
* @param [permissions] 权限
|
||||
*/
|
||||
data class JibProject(
|
||||
var projectName: String = "",
|
||||
var ports: List<String> = listOf("8080"),
|
||||
var environment: Map<String, String> = mapOf("SPRING_PROFILES_ACTIVE" to "prod"),
|
||||
var entrypoint: List<String> = emptyList(),
|
||||
var paths: List<String> = listOf("llmhub-base/scripts/entrypoint.sh"),
|
||||
var imageName: String = "",
|
||||
var version: String = "latest",
|
||||
var permissions: Map<String, String> = mapOf("/scripts/entrypoint.sh" to "755"),
|
||||
var baseImage: String = "docker://bellsoft/liberica-openjdk-debian:21"
|
||||
) {
|
||||
init {
|
||||
if (imageName.isEmpty()) {
|
||||
imageName = projectName
|
||||
}
|
||||
}
|
||||
}
|
||||
54
llmhub-base/docker/docker-compose.master.yml
Normal file
54
llmhub-base/docker/docker-compose.master.yml
Normal file
@ -0,0 +1,54 @@
|
||||
services:
|
||||
nacos:
|
||||
image: nacos/nacos-server:latest
|
||||
container_name: nacos-server
|
||||
ports:
|
||||
- "9001:8848" # Nacos控制台
|
||||
- "9848:9848" # gRPC (nacos2.0以后内部使用)
|
||||
- "9849:9849" # gRPC (nacos2.0以后内部使用)
|
||||
environment:
|
||||
- NACOS_AUTH_ENABLE=true
|
||||
- NACOS_AUTH_IDENTITY_KEY=serverIdentity
|
||||
- NACOS_AUTH_IDENTITY_VALUE=security
|
||||
- NACOS_AUTH_TOKEN=L4s6f9y3
|
||||
- MODE=standalone
|
||||
- SPRING_DATASOURCE_PLATFORM=empty
|
||||
- JVM_XMS=256m
|
||||
- JVM_XMX=512m
|
||||
networks:
|
||||
- llmhub-net
|
||||
volumes:
|
||||
- nacos-conf-volume:/home/nacos/conf
|
||||
- nacos-data-volume:/home/nacos/data
|
||||
- nacos-logs-volume:/home/nacos/logs
|
||||
restart: always
|
||||
|
||||
llmhub-core-service:
|
||||
image: ${49.235.96.75:5000}/llmhub-core-service
|
||||
container_name: llmhub-core-service
|
||||
ports:
|
||||
- "9002:9002"
|
||||
networks:
|
||||
- llmhub-net
|
||||
volumes:
|
||||
- llmhub-core-service-volume:/app/volume
|
||||
restart: always
|
||||
llmhub-impl-baiLian:
|
||||
image: ${49.235.96.75:5000}/llmhub-impl-baiLian
|
||||
container_name: llmhub-impl-baiLian
|
||||
ports:
|
||||
- "9002:9002"
|
||||
networks:
|
||||
- llmhub-net
|
||||
volumes:
|
||||
- llmhub-impl-baiLian-volume:/app/volume
|
||||
restart: always
|
||||
networks:
|
||||
llmhub-net:
|
||||
driver: bridge
|
||||
volumes:
|
||||
nacos-conf-volume:
|
||||
nacos-data-volume:
|
||||
nacos-logs-volume:
|
||||
llmhub-core-service-volume:
|
||||
llmhub-impl-baiLian-volume:
|
||||
@ -1,12 +1,13 @@
|
||||
[versions]
|
||||
kotlin-version = "2.0.0"
|
||||
spring-cloud-version = "2024.0.1"
|
||||
spring-cloud-starter-version = "4.2.1"
|
||||
spring-boot-version = "3.4.4"
|
||||
spring-cloud-version = "2023.0.5"
|
||||
spring-boot-version = "3.2.4"
|
||||
spring-dependency-management-version = "1.1.7"
|
||||
aliyun-bailian-version = "2.0.0"
|
||||
spring-cloud-starter-alibaba-nacos-discovery-version = "2023.0.3.2"
|
||||
forgeBoot-version = "1.0.0"
|
||||
okHttp-version = "4.12.0"
|
||||
jib-version = "3.4.2"
|
||||
[plugins]
|
||||
# 应用 Java 插件,提供基本的 Java 代码编译和构建能力
|
||||
java = { id = "java" }
|
||||
@ -24,19 +25,22 @@ spring-dependency-management = { id = "io.spring.dependency-management", version
|
||||
# 应用 Spring Boot 插件,提供 Spring Boot 应用的开发和运行能力
|
||||
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot-version" }
|
||||
|
||||
jib = { id = "com.google.cloud.tools.jib", version.ref = "jib-version" }
|
||||
jibLocalPlugin = { id = "org.jcnc.llmhub.plugin.jib" }
|
||||
[libraries]
|
||||
jib-gradlePlugin = { module = "com.google.cloud.tools.jib:com.google.cloud.tools.jib.gradle.plugin", version.ref = "jib-version" }
|
||||
# bom
|
||||
springCloudDependencies-bom = { module = "org.springframework.cloud:spring-cloud-dependencies", version.ref = "spring-cloud-version" }
|
||||
springCloudDependencies-bom = { group ="org.springframework.cloud",name = "spring-cloud-dependencies", version.ref = "spring-cloud-version" }
|
||||
# kotlinx
|
||||
# 响应式协程库
|
||||
kotlinx-coruntes-reactor = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-reactor"}
|
||||
kotlinx-coruntes-reactor = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-reactor" }
|
||||
|
||||
# 阿里云百炼
|
||||
aliyun-bailian = { group = "com.aliyun", name = "bailian20231229", version.ref = "aliyun-bailian-version" }
|
||||
|
||||
# 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" ,version.ref="spring-cloud-starter-version"}
|
||||
springCloudStarter-loadbalancer = { group = "org.springframework.cloud", name = "spring-cloud-starter-loadbalancer" }
|
||||
|
||||
# SpringBootStarter
|
||||
springBootStarter-webflux = { group = "org.springframework.boot", name = "spring-boot-starter-webflux" }
|
||||
@ -45,7 +49,9 @@ springBootStarter-test = { group = "org.springframework.boot", name = "spring-bo
|
||||
|
||||
junitPlatform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" }
|
||||
|
||||
# OkHttp
|
||||
okHttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okHttp-version" }
|
||||
# forgeBoot
|
||||
forgeBoot-webmvc-version-springBootStarter = { group = "com.gewuyou.forgeboot", name = "forgeboot-webmvc-version-spring-boot-starter",version.ref="forgeBoot-version" }
|
||||
forgeBoot-core-extension = { group = "com.gewuyou.forgeboot", name = "forgeboot-core-extension",version.ref="forgeBoot-version" }
|
||||
forgeBoot-webmvc-version-springBootStarter = { group = "com.gewuyou.forgeboot", name = "forgeboot-webmvc-version-spring-boot-starter", version.ref = "forgeBoot-version" }
|
||||
forgeBoot-core-extension = { group = "com.gewuyou.forgeboot", name = "forgeboot-core-extension", version.ref = "forgeBoot-version" }
|
||||
[bundles]
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
extra {
|
||||
// 开启springboot
|
||||
setProperty(ProjectFlags.USE_SPRING_BOOT, true)
|
||||
setProperty(ProjectFlags.USE_SPRING_CLOUD_BOM,true)
|
||||
}
|
||||
dependencies {
|
||||
val libs = rootProject.libs
|
||||
@ -11,6 +12,7 @@ dependencies {
|
||||
implementation(libs.springBootStarter.webflux)
|
||||
implementation(libs.springCloudStarter.loadbalancer)
|
||||
|
||||
|
||||
implementation(project(Modules.Core.SPI))
|
||||
|
||||
// Kotlin Coroutines
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
package org.jcnc.llmhub.core.service
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableDiscoveryClient
|
||||
class LlmhubCoreServiceApplication
|
||||
|
||||
/**
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package org.jcnc.llmhub.core.service.config
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope
|
||||
|
||||
/**
|
||||
*模型属性
|
||||
@ -9,7 +10,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
* @author gewuyou
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "llmhub.model-route")
|
||||
class ModelProperties {
|
||||
@RefreshScope
|
||||
open class ModelProperties {
|
||||
/**
|
||||
* 模型名前缀 -> 服务名映射
|
||||
* 该映射表存储了模型名前缀与服务名的对应关系,用于快速查找模型对应的服务
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
package org.jcnc.llmhub.core.service.controller
|
||||
|
||||
import com.gewuyou.forgeboot.webmvc.version.annotation.ApiVersion
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.jcnc.llmhub.core.service.service.impl.LLMServiceImpl
|
||||
import org.jcnc.llmhub.core.spi.entities.request.ChatRequest
|
||||
import org.jcnc.llmhub.core.spi.entities.response.ChatResponsePart
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@ -17,5 +21,18 @@ import org.springframework.web.bind.annotation.RestController
|
||||
class ChatController(
|
||||
private val llmServiceImpl: LLMServiceImpl
|
||||
) {
|
||||
|
||||
/**
|
||||
* 聊天功能入口
|
||||
*
|
||||
* 该函数负责调用语言模型服务实现类中的聊天方法,为用户提供与AI聊天的功能
|
||||
* 它接收一个ChatRequest对象作为参数,该对象包含了聊天所需的参数和用户信息
|
||||
* 函数返回一个Flow流,流中包含了部分聊天响应,这种设计允许用户逐步接收聊天结果,提高用户体验
|
||||
*
|
||||
* @param request 聊天请求对象,包含了发起聊天所需的各种参数和用户信息
|
||||
* @return 返回一个Flow流,流中依次提供了聊天响应的部分数据,允许异步处理和逐步消费响应内容
|
||||
*/
|
||||
@PostMapping("/stream")
|
||||
fun chat(request: ChatRequest): Flow<ChatResponsePart> {
|
||||
return llmServiceImpl.chat(request)
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
package org.jcnc.llmhub.core.service.manager
|
||||
|
||||
import org.jcnc.llmhub.core.service.config.ModelProperties
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
/**
|
||||
@ -14,7 +13,6 @@ import org.springframework.stereotype.Component
|
||||
* @author gewuyou
|
||||
*/
|
||||
@Component
|
||||
@RefreshScope // 支持Nacos热刷新,使得配置更新时无需重启应用即可生效
|
||||
class ModelRouteManager(
|
||||
private val modelProperties: ModelProperties
|
||||
) {
|
||||
|
||||
@ -3,11 +3,20 @@ server:
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
access-key: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTc0NTc1NjkwOH0.86TmeN27gXbpw55jMmOVNz42B9u8dXtGwCvyGlWbYVo
|
||||
username: nacos
|
||||
password: L4s6f9y3
|
||||
server-addr: 49.235.96.75:8848
|
||||
discovery:
|
||||
server-addr: 49.235.96.75:8848 # Nacos 服务地址
|
||||
server-addr: ${spring.cloud.nacos.server-addr}
|
||||
access-key: ${spring.cloud.nacos.access-key}
|
||||
username: ${spring.cloud.nacos.username}
|
||||
password: ${spring.cloud.nacos.password}
|
||||
llmhub:
|
||||
model-route:
|
||||
modelServiceMap:
|
||||
openai: llmhub-impl-openai
|
||||
anotherModel: llmhub-impl-another
|
||||
qwen-turbo: llmhub-impl-baiLian
|
||||
qwen-max: llmhub-impl-baiLian
|
||||
qwen-plus: llmhub-impl-baiLian
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,25 @@
|
||||
spring:
|
||||
application:
|
||||
name: llmhub-core-service
|
||||
profiles:
|
||||
active: dev
|
||||
cloud:
|
||||
nacos:
|
||||
access-key: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTc0NTc1NjkwOH0.86TmeN27gXbpw55jMmOVNz42B9u8dXtGwCvyGlWbYVo
|
||||
username: nacos
|
||||
password: L4s6f9y3
|
||||
server-addr: 49.235.96.75:8848
|
||||
discovery:
|
||||
server-addr: ${spring.cloud.nacos.server-addr}
|
||||
access-key: ${spring.cloud.nacos.access-key}
|
||||
username: ${spring.cloud.nacos.username}
|
||||
password: ${spring.cloud.nacos.password}
|
||||
llmhub:
|
||||
model-route:
|
||||
modelServiceMap:
|
||||
qwen-turbo: llmhub-impl-baiLian
|
||||
qwen-max: llmhub-impl-baiLian
|
||||
qwen-plus: llmhub-impl-baiLian
|
||||
|
||||
# profiles:
|
||||
# active: dev
|
||||
server:
|
||||
port: 8081
|
||||
@ -4,7 +4,6 @@ apply {
|
||||
plugin(libs.plugins.kotlin.plugin.spring.get().pluginId)
|
||||
}
|
||||
dependencies {
|
||||
val libs = rootProject.libs
|
||||
compileOnly(libs.kotlinx.coruntes.reactor)
|
||||
compileOnly(libs.springBootStarter.web)
|
||||
}
|
||||
@ -13,5 +13,5 @@ package org.jcnc.llmhub.core.spi.entities.request
|
||||
data class ChatRequest(
|
||||
val prompt: String,
|
||||
val model: String,
|
||||
val options: Map<String, Any>? = null
|
||||
val options: Map<String, String> = mapOf()
|
||||
)
|
||||
|
||||
@ -1,7 +1,19 @@
|
||||
|
||||
// 开启springboot
|
||||
extra[ProjectFlags.USE_SPRING_BOOT] = true
|
||||
setProperty(ProjectFlags.USE_SPRING_CLOUD_BOM,true)
|
||||
dependencies {
|
||||
// Nacos 服务发现和配置
|
||||
implementation(libs.springCloudStarter.alibaba.nacos.discovery)
|
||||
|
||||
implementation(project(Modules.Core.SPI))
|
||||
|
||||
implementation(libs.kotlinx.coruntes.reactor)
|
||||
|
||||
implementation(libs.aliyun.bailian)
|
||||
|
||||
implementation(libs.okHttp)
|
||||
|
||||
implementation(libs.forgeBoot.core.extension)
|
||||
}
|
||||
|
||||
|
||||
@ -2,9 +2,11 @@ package org.jcnc.llmhub.impl.baiLian
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableDiscoveryClient
|
||||
class LlmhubImplBaiLianApplication
|
||||
fun main(args: Array<String>) {
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<LlmhubImplBaiLianApplication>(*args)
|
||||
}
|
||||
|
||||
@ -0,0 +1,107 @@
|
||||
package org.jcnc.llmhub.impl.baiLian.adapter
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import okhttp3.Headers.Companion.toHeaders
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.jcnc.llmhub.core.spi.entities.response.ChatResponsePart
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
/**
|
||||
* 百炼适配器
|
||||
*
|
||||
* 该类负责与百炼API进行交互,提供流式聊天功能。
|
||||
* 它使用OkHttpClient发送请求,并通过Jackson库处理JSON数据。
|
||||
*
|
||||
* @param okHttpClient 用于发送HTTP请求的客户端
|
||||
* @param objectMapper 用于序列化和反序列化JSON数据的映射器
|
||||
* @since 2025-04-27 10:31:47
|
||||
* @author gewuyou
|
||||
*/
|
||||
@Component
|
||||
class DashScopeAdapter(
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val objectMapper: ObjectMapper
|
||||
) {
|
||||
/**
|
||||
* 发送流式聊天请求
|
||||
*
|
||||
* 本函数构建并发送一个聊天请求,然后以流的形式接收和处理响应。
|
||||
* 它主要用于与DashScope API进行交互,提取并发布聊天响应的部分内容。
|
||||
*
|
||||
* @param url 请求的URL
|
||||
* @param headers 请求的头部信息
|
||||
* @param requestBody 请求的主体内容
|
||||
* @param extractContent 一个函数,用于从JSON响应中提取内容
|
||||
* @param dispatcher 协程调度器,默认为IO调度器
|
||||
* @return 返回一个Flow,发布聊天响应的部分内容
|
||||
*/
|
||||
fun sendStreamChat(
|
||||
url: String,
|
||||
headers: Map<String, String>,
|
||||
requestBody: Any,
|
||||
extractContent: (String) -> String,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
): Flow<ChatResponsePart> = flow {
|
||||
// 将请求体序列化为JSON格式
|
||||
val requestJson = objectMapper.writeValueAsString(requestBody)
|
||||
// 构建HTTP请求
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.headers(headers.toHeaders())
|
||||
.post(requestJson.toRequestBody("application/json".toMediaType()))
|
||||
.build()
|
||||
|
||||
// 发送HTTP请求
|
||||
val call = okHttpClient.newCall(request)
|
||||
|
||||
// 使用指定的调度器执行请求
|
||||
val response = withContext(dispatcher) {
|
||||
call.execute()
|
||||
}
|
||||
|
||||
// 检查HTTP响应是否成功
|
||||
if (!response.isSuccessful) {
|
||||
throw RuntimeException("DashScope request failed: ${response.code}")
|
||||
}
|
||||
|
||||
// 获取响应体
|
||||
val responseBody = response.body ?: throw RuntimeException("Empty response body from DashScope")
|
||||
// 获取响应体的源
|
||||
val source = responseBody.source().buffer
|
||||
|
||||
try {
|
||||
// 读取响应体,直到数据结束或协程被取消
|
||||
while (!source.exhausted() && currentCoroutineContext().isActive) {
|
||||
// 读取一行数据
|
||||
val line = source.readUtf8Line()
|
||||
// 忽略空行
|
||||
if (line.isNullOrBlank()) {
|
||||
continue
|
||||
}
|
||||
// 处理以"data:"开头的行
|
||||
if (line.startsWith("data:")) {
|
||||
// 提取JSON部分
|
||||
val jsonPart = line.removePrefix("data:").trim()
|
||||
// 提取内容
|
||||
val content = extractContent(jsonPart)
|
||||
|
||||
// 发布聊天响应的部分内容
|
||||
emit(ChatResponsePart(content = content, done = false))
|
||||
}
|
||||
}
|
||||
|
||||
// 发布结束信号
|
||||
emit(ChatResponsePart(content = "[END]", done = true))
|
||||
} finally {
|
||||
// 关闭资源
|
||||
source.close()
|
||||
response.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package org.jcnc.llmhub.impl.baiLian.config
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
/**
|
||||
*客户端配置
|
||||
*
|
||||
* @since 2025-03-28 17:03:48
|
||||
* @author gewuyou
|
||||
*/
|
||||
@Configuration
|
||||
class ClientConfig {
|
||||
/**
|
||||
* OkHttpClient
|
||||
*/
|
||||
@Bean
|
||||
fun createOkHttpClient(): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package org.jcnc.llmhub.impl.baiLian.config
|
||||
|
||||
import org.jcnc.llmhub.impl.baiLian.config.entities.DashScopeProperties
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
/**
|
||||
*仪表范围配置
|
||||
*
|
||||
* @since 2025-04-27 10:53:55
|
||||
* @author gewuyou
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(DashScopeProperties::class)
|
||||
class DashScopeConfig
|
||||
@ -0,0 +1,48 @@
|
||||
package org.jcnc.llmhub.impl.baiLian.config.entities
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
|
||||
/**
|
||||
* 仪表范围属性
|
||||
*
|
||||
* @author gewuyou
|
||||
* @since 2025-03-08 15:39:34
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "aliyun.dash.scope")
|
||||
class DashScopeProperties {
|
||||
/**
|
||||
* 访问密钥ID,用于身份认证。
|
||||
*/
|
||||
var accessKeyId: String = ""
|
||||
|
||||
/**
|
||||
* 访问密钥,与访问密钥ID配合使用完成身份认证。
|
||||
*/
|
||||
var accessKeySecret: String = ""
|
||||
|
||||
/**
|
||||
* 服务访问端点,例如 https://dash.aliyun.com。
|
||||
*/
|
||||
var endpoint: String = ""
|
||||
|
||||
/**
|
||||
* 工作空间ID,标识具体的业务工作空间。
|
||||
*/
|
||||
var workspaceId: String = ""
|
||||
|
||||
/**
|
||||
* API密钥,用于调用API的身份验证。
|
||||
*/
|
||||
var apiKey: String = ""
|
||||
|
||||
/**
|
||||
* 应用ID,标识具体的应用程序。
|
||||
*/
|
||||
var appId: String = ""
|
||||
|
||||
/**
|
||||
* 应用调用基路径,例如 /api/v1。
|
||||
*/
|
||||
var baseUrl: String = ""
|
||||
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package org.jcnc.llmhub.impl.baiLian.controller
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.jcnc.llmhub.core.spi.entities.request.ChatRequest
|
||||
import org.jcnc.llmhub.core.spi.entities.request.EmbeddingRequest
|
||||
import org.jcnc.llmhub.core.spi.entities.response.ChatResponsePart
|
||||
import org.jcnc.llmhub.core.spi.entities.response.EmbeddingResponse
|
||||
import org.jcnc.llmhub.core.spi.provider.LLMProvider
|
||||
import org.jcnc.llmhub.impl.baiLian.service.BaiLianModelService
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
/**
|
||||
*百炼提供商
|
||||
*
|
||||
* @since 2025-04-27 10:12:12
|
||||
* @author gewuyou
|
||||
*/
|
||||
@RestController
|
||||
class BaiLianProvider(
|
||||
private val baiLianModelService: BaiLianModelService
|
||||
): LLMProvider {
|
||||
/**
|
||||
* 初始化与聊天服务的连接,以处理聊天请求
|
||||
*
|
||||
* 此函数接收一个聊天请求对象,并返回一个Flow流,用于接收聊天响应的部分数据
|
||||
* 它主要用于建立聊天通信的通道,而不是发送具体的消息
|
||||
*
|
||||
* @param request 聊天请求对象,包含建立聊天所需的信息,如用户标识、会话标识等
|
||||
* @return 返回一个Flow流,通过该流可以接收到聊天响应的部分数据,如消息、状态更新等
|
||||
*/
|
||||
override fun chat(request: ChatRequest): Flow<ChatResponsePart> {
|
||||
return baiLianModelService.streamChat(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* 嵌入功能方法
|
||||
* 该方法允许用户发送嵌入请求,以获取LLM生成的嵌入向量
|
||||
*
|
||||
* @param request 嵌入请求对象,包含需要进行嵌入处理的数据
|
||||
* @return EmbeddingResponse 嵌入响应对象,包含生成的嵌入向量信息
|
||||
*/
|
||||
override fun embedding(request: EmbeddingRequest): EmbeddingResponse {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package org.jcnc.llmhub.impl.baiLian.service
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.jcnc.llmhub.core.spi.entities.request.ChatRequest
|
||||
import org.jcnc.llmhub.core.spi.entities.response.ChatResponsePart
|
||||
|
||||
/**
|
||||
* 百炼模型服务接口
|
||||
* 用于定义与百炼AI模型交互的方法
|
||||
*
|
||||
* @since 2025-04-27 10:45:42
|
||||
* @author gewuyou
|
||||
*/
|
||||
interface BaiLianModelService {
|
||||
/**
|
||||
* 使用流式聊天交互
|
||||
*
|
||||
* @param request 聊天请求对象,包含用户输入、上下文等信息
|
||||
* @return 返回一个Flow流,包含部分聊天响应,允许逐步处理和消费响应
|
||||
*/
|
||||
fun streamChat(request: ChatRequest): Flow<ChatResponsePart>
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
package org.jcnc.llmhub.impl.baiLian.service.impl
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.jcnc.llmhub.core.spi.entities.request.ChatRequest
|
||||
import org.jcnc.llmhub.core.spi.entities.response.ChatResponsePart
|
||||
import org.jcnc.llmhub.impl.baiLian.adapter.DashScopeAdapter
|
||||
import org.jcnc.llmhub.impl.baiLian.config.entities.DashScopeProperties
|
||||
import org.jcnc.llmhub.impl.baiLian.service.BaiLianModelService
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.util.CollectionUtils
|
||||
import org.springframework.util.StringUtils
|
||||
/**
|
||||
* 百炼模型服务接口实现
|
||||
*
|
||||
* @since 2025-04-27 10:47:07
|
||||
* @author gewuyou
|
||||
*/
|
||||
@Service
|
||||
class BaiLianModelServiceImpl(
|
||||
private val dashScopeAdapter: DashScopeAdapter,
|
||||
private val dashScopeProperties: DashScopeProperties,
|
||||
private val objectMapper: ObjectMapper
|
||||
) : BaiLianModelService {
|
||||
/**
|
||||
* 使用流式聊天交互
|
||||
*
|
||||
* @param request 聊天请求对象,包含用户输入、上下文等信息
|
||||
* @return 返回一个Flow流,包含部分聊天响应,允许逐步处理和消费响应
|
||||
*/
|
||||
override fun streamChat(request: ChatRequest): Flow<ChatResponsePart> {
|
||||
// 构造请求URL
|
||||
val url = "${dashScopeProperties.baseUrl}${dashScopeProperties.appId}/completion"
|
||||
// 构造请求头,包括授权信息和内容类型
|
||||
val headers = mapOf(
|
||||
"Authorization" to "Bearer ${dashScopeProperties.apiKey}",
|
||||
"Content-Type" to "application/json",
|
||||
"X-DashScope-SSE" to "enable"
|
||||
)
|
||||
// 构造输入参数,主要包括用户的prompt
|
||||
val inputMap = mutableMapOf("prompt" to request.prompt)
|
||||
// 获取会话ID,如果存在,则添加到输入参数中
|
||||
val sessionId = (request.options["session_id"] ?: "").toString()
|
||||
if (StringUtils.hasText(sessionId)) {
|
||||
inputMap["session_id"] = sessionId
|
||||
}
|
||||
// 构造参数地图,包括是否包含思考、模型ID、增量输出等
|
||||
val parametersMap = mutableMapOf(
|
||||
"has_thoughts" to (request.options["has_thoughts"] ?: false),
|
||||
"model_id" to request.model,
|
||||
"incremental_output" to true,
|
||||
"rag_options" to emptyMap<String, Any>()
|
||||
)
|
||||
// 构造RAG选项参数,包括管道ID和文件ID
|
||||
val ragOptionsMap = mutableMapOf<String, Any>()
|
||||
// 解析并添加管道ID
|
||||
val pipelineIdsJson = request.options["pipelineIds"]
|
||||
pipelineIdsJson?.let {
|
||||
val pipelineIds = objectMapper.readValue(it, object : TypeReference<List<String>>() {})
|
||||
if (!CollectionUtils.isEmpty(pipelineIds)) {
|
||||
ragOptionsMap["pipeline_ids"] = pipelineIds
|
||||
}
|
||||
}
|
||||
// 解析并添加文件ID
|
||||
val fileIdsJson = request.options["fileIds"]
|
||||
fileIdsJson?.let {
|
||||
val fileIds = objectMapper.readValue(it, object : TypeReference<List<String>>() {})
|
||||
if (!CollectionUtils.isEmpty(fileIds)) {
|
||||
ragOptionsMap["session_file_ids"] = fileIds
|
||||
}
|
||||
}
|
||||
// 将RAG选项参数添加到参数地图中
|
||||
parametersMap["rag_options"] = ragOptionsMap
|
||||
// 构造请求体,包括输入参数、构造的参数地图和调试信息
|
||||
val body = mapOf(
|
||||
"input" to inputMap,
|
||||
"parameters" to parametersMap,
|
||||
"debug" to emptyMap()
|
||||
)
|
||||
// 发送流式聊天请求,并处理响应
|
||||
return dashScopeAdapter.sendStreamChat(
|
||||
url, headers, body,
|
||||
{ json: String ->
|
||||
// 解析响应JSON,提取输出文本
|
||||
val node = objectMapper.readTree(json)
|
||||
node["output"]?.get("text")?.asText() ?: ""
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
server:
|
||||
port: 8082
|
||||
spring:
|
||||
application:
|
||||
name: llmhub-impl-baiLian
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: 49.235.96.75:8848 # Nacos 服务地址
|
||||
# 阿里云配置
|
||||
aliyun:
|
||||
# DashScope服务配置
|
||||
dash:
|
||||
# 访问凭证配置
|
||||
scope:
|
||||
access-key-id: LTAI5tHiA2Ry3XTAfoSEJW6z # 阿里云访问密钥ID
|
||||
access-key-secret: K5sf4FxZZuUgLEFnyfepBfMqFGmDcD # 阿里云访问密钥密钥
|
||||
endpoint: bailian.cn-beijing.aliyuncs.com # 阿里云服务端点
|
||||
workspace-id: llm-axfkuqft05uzbjpi # 工作区ID
|
||||
api-key: sk-78af4dd964a94f4cb373851064dbdc12 # API密钥
|
||||
app-id: 3fae0bbab2e54a90a37aa02cd12dd62c # 应用ID
|
||||
base-url: https://dashscope.aliyuncs.com/api/v1/apps/ # 基础API URL
|
||||
@ -0,0 +1,22 @@
|
||||
server:
|
||||
port: 9003
|
||||
spring:
|
||||
application:
|
||||
name: llmhub-impl-baiLian
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: 49.235.96.75:9001 # Nacos 服务地址
|
||||
# 阿里云配置
|
||||
aliyun:
|
||||
# DashScope服务配置
|
||||
dash:
|
||||
# 访问凭证配置
|
||||
scope:
|
||||
access-key-id: LTAI5tHiA2Ry3XTAfoSEJW6z # 阿里云访问密钥ID
|
||||
access-key-secret: K5sf4FxZZuUgLEFnyfepBfMqFGmDcD # 阿里云访问密钥密钥
|
||||
endpoint: bailian.cn-beijing.aliyuncs.com # 阿里云服务端点
|
||||
workspace-id: llm-axfkuqft05uzbjpi # 工作区ID
|
||||
api-key: sk-78af4dd964a94f4cb373851064dbdc12 # API密钥
|
||||
app-id: 3fae0bbab2e54a90a37aa02cd12dd62c # 应用ID
|
||||
base-url: https://dashscope.aliyuncs.com/api/v1/apps/ # 基础API URL
|
||||
@ -1,3 +1,18 @@
|
||||
spring:
|
||||
application:
|
||||
name: llmhub-impl-baiLian
|
||||
profiles:
|
||||
active: dev
|
||||
# 阿里云配置
|
||||
aliyun:
|
||||
# DashScope服务配置
|
||||
dash:
|
||||
# 访问凭证配置
|
||||
scope:
|
||||
access-key-id: LTAI5tHiA2Ry3XTAfoSEJW6z # 阿里云访问密钥ID
|
||||
access-key-secret: K5sf4FxZZuUgLEFnyfepBfMqFGmDcD # 阿里云访问密钥密钥
|
||||
endpoint: bailian.cn-beijing.aliyuncs.com # 阿里云服务端点
|
||||
workspace-id: llm-axfkuqft05uzbjpi # 工作区ID
|
||||
api-key: sk-78af4dd964a94f4cb373851064dbdc12 # API密钥
|
||||
app-id: 3fae0bbab2e54a90a37aa02cd12dd62c # 应用ID
|
||||
base-url: https://dashscope.aliyuncs.com/api/v1/apps/ # 基础API URL
|
||||
99
llmhub-base/scripts/entrypoint.sh
Normal file
99
llmhub-base/scripts/entrypoint.sh
Normal file
@ -0,0 +1,99 @@
|
||||
#!/bin/bash
|
||||
#set -x # 调试模式,可以启用以打印每行脚本的执行情况
|
||||
|
||||
# 设置默认的等待时间间隔,默认值为2秒
|
||||
: "${SLEEP_SECOND:=2}"
|
||||
# 设置默认的超时时间,默认值为60秒
|
||||
: "${TIMEOUT:=60}"
|
||||
|
||||
# 带时间戳+emoji的小型日志函数
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||
}
|
||||
|
||||
# 等待服务函数
|
||||
wait_for() {
|
||||
local host="$1"
|
||||
local port="$2"
|
||||
local timeout="$TIMEOUT"
|
||||
local start_time
|
||||
local current_time
|
||||
local elapsed_time
|
||||
local attempt=0
|
||||
|
||||
start_time=$(date +%s)
|
||||
log "🔄 开始等待依赖服务 $host:$port 可用 (超时时间: ${timeout}s, 间隔: ${SLEEP_SECOND}s)"
|
||||
|
||||
while ! nc -z "$host" "$port" 2>/dev/null; do
|
||||
current_time=$(date +%s)
|
||||
elapsed_time=$((current_time - start_time))
|
||||
attempt=$((attempt + 1))
|
||||
|
||||
if [ "$elapsed_time" -ge "$timeout" ]; then
|
||||
log "🛑 [ERROR] 等待超时!依赖服务 $host:$port 在 ${timeout}s 内未启动"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "🔄 第 ${attempt} 次检测:$host:$port 未就绪,已等待 ${elapsed_time}s..."
|
||||
sleep "$SLEEP_SECOND"
|
||||
done
|
||||
|
||||
total_time=$(( $(date +%s) - start_time ))
|
||||
log "✅ [SUCCESS] 依赖服务 $host:$port 已启动,耗时 ${total_time}s"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 声明变量
|
||||
declare DEPENDS
|
||||
declare CMD
|
||||
|
||||
# 解析参数
|
||||
while getopts "d:c:" arg; do
|
||||
case "$arg" in
|
||||
d)
|
||||
DEPENDS="$OPTARG"
|
||||
;;
|
||||
c)
|
||||
CMD="$OPTARG"
|
||||
;;
|
||||
?)
|
||||
log "🛑 [ERROR] 未知参数"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 检查依赖
|
||||
if [ -n "$DEPENDS" ]; then
|
||||
log "📦 检测到依赖服务列表: $DEPENDS"
|
||||
for var in ${DEPENDS//,/ }; do
|
||||
host=${var%:*}
|
||||
port=${var#*:}
|
||||
if [ -z "$host" ] || [ -z "$port" ]; then
|
||||
log "🛑 [ERROR] 依赖项格式错误: $var,应为 host:port"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! wait_for "$host" "$port"; then
|
||||
log "❌ [ERROR] 依赖服务 $host:$port 启动失败,终止执行"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
else
|
||||
log "⚡️ 未配置依赖服务,跳过依赖检测"
|
||||
fi
|
||||
|
||||
# 执行命令
|
||||
if [ -n "$CMD" ]; then
|
||||
log "🚀 准备执行命令: $CMD"
|
||||
eval "$CMD"
|
||||
cmd_exit_code=$?
|
||||
if [ $cmd_exit_code -eq 0 ]; then
|
||||
log "✅ [SUCCESS] 命令执行完成,退出码: $cmd_exit_code"
|
||||
else
|
||||
log "❌ [ERROR] 命令执行失败,退出码: $cmd_exit_code"
|
||||
exit $cmd_exit_code
|
||||
fi
|
||||
else
|
||||
log "⚠️ [WARNING] 未指定要执行的命令,脚本结束"
|
||||
fi
|
||||
Loading…
x
Reference in New Issue
Block a user