feat: switchable router embedder and L3 backends via config
Adds [router.embedder] and [router.l3] sections to CorrexConfig with backend selectors. Ships LlamaCppEmbedder that hits llama.cpp's /embedding endpoint (handles OpenAI-compatible, simple, and array response shapes; validates dimension). InfrastructureModule gains createEmbedderFromConfig and createL3MemoryStoreFromConfig that dispatch on backend value. Defaults preserve current behavior (noop embedder + in-memory L3). Switching to "llamacpp" / "turbovec" is a config-only change — no code edits required. For turbovec backend, the bundled python sidecar script is extracted from classpath to ~/.cache/correx/ on first use.
This commit is contained in:
@@ -124,12 +124,16 @@ fun main() {
|
|||||||
sandboxRoot = sandboxRoot,
|
sandboxRoot = sandboxRoot,
|
||||||
defaultSystemPromptPath = toolsConfig.defaultSystemPromptPath,
|
defaultSystemPromptPath = toolsConfig.defaultSystemPromptPath,
|
||||||
)
|
)
|
||||||
val routerConfig = RouterConfig()
|
val routerConfig = correxConfig.router
|
||||||
|
val embedder = InfrastructureModule.createEmbedderFromConfig(routerConfig.embedder)
|
||||||
|
val l3MemoryStore = InfrastructureModule.createL3MemoryStoreFromConfig(routerConfig.l3)
|
||||||
val routerFacade = InfrastructureModule.createRouterFacade(
|
val routerFacade = InfrastructureModule.createRouterFacade(
|
||||||
eventStore = eventStore,
|
eventStore = eventStore,
|
||||||
inferenceRouter = inferenceRouter,
|
inferenceRouter = inferenceRouter,
|
||||||
config = routerConfig,
|
config = RouterConfig(),
|
||||||
tokenizer = llamaProvider.tokenizer,
|
tokenizer = llamaProvider.tokenizer,
|
||||||
|
embedder = embedder,
|
||||||
|
l3MemoryStore = l3MemoryStore,
|
||||||
)
|
)
|
||||||
val module = ServerModule(
|
val module = ServerModule(
|
||||||
orchestrator = orchestrator,
|
orchestrator = orchestrator,
|
||||||
|
|||||||
@@ -210,6 +210,9 @@ object ConfigLoader {
|
|||||||
val toolsFileReadSection = sections["tools.file_read"] ?: emptyMap()
|
val toolsFileReadSection = sections["tools.file_read"] ?: emptyMap()
|
||||||
val toolsFileWriteSection = sections["tools.file_write"] ?: emptyMap()
|
val toolsFileWriteSection = sections["tools.file_write"] ?: emptyMap()
|
||||||
val toolsFileEditSection = sections["tools.file_edit"] ?: emptyMap()
|
val toolsFileEditSection = sections["tools.file_edit"] ?: emptyMap()
|
||||||
|
val routerSection = sections["router"] ?: emptyMap()
|
||||||
|
val routerEmbedderSection = sections["router.embedder"] ?: emptyMap()
|
||||||
|
val routerL3Section = sections["router.l3"] ?: emptyMap()
|
||||||
|
|
||||||
val server = ServerConfig(
|
val server = ServerConfig(
|
||||||
host = asString(serverSection["host"], "localhost"),
|
host = asString(serverSection["host"], "localhost"),
|
||||||
@@ -322,6 +325,27 @@ object ConfigLoader {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val embedder = EmbedderConfig(
|
||||||
|
backend = asString(routerEmbedderSection["backend"], "noop"),
|
||||||
|
dimension = asInt(routerEmbedderSection["dimension"], 1536),
|
||||||
|
url = asStringOrNull(routerEmbedderSection["url"]),
|
||||||
|
modelId = asStringOrNull(routerEmbedderSection["model_id"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
val l3 = L3Config(
|
||||||
|
backend = asString(routerL3Section["backend"], "in_memory"),
|
||||||
|
persistPath = asStringOrNull(routerL3Section["persist_path"]),
|
||||||
|
pythonExecutable = asString(routerL3Section["python_executable"], "python3"),
|
||||||
|
scriptPath = asStringOrNull(routerL3Section["script_path"]),
|
||||||
|
dim = asInt(routerL3Section["dim"], 1536),
|
||||||
|
bitWidth = asInt(routerL3Section["bit_width"], 4),
|
||||||
|
)
|
||||||
|
|
||||||
|
val router = RouterConfig(
|
||||||
|
embedder = embedder,
|
||||||
|
l3 = l3,
|
||||||
|
)
|
||||||
|
|
||||||
return CorrexConfig(
|
return CorrexConfig(
|
||||||
server = server,
|
server = server,
|
||||||
tui = tui,
|
tui = tui,
|
||||||
@@ -329,6 +353,7 @@ object ConfigLoader {
|
|||||||
approval = approval,
|
approval = approval,
|
||||||
tools = tools,
|
tools = tools,
|
||||||
providers = providers,
|
providers = providers,
|
||||||
|
router = router,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ data class CorrexConfig(
|
|||||||
val approval: ApprovalConfig = ApprovalConfig(),
|
val approval: ApprovalConfig = ApprovalConfig(),
|
||||||
val tools: ToolsConfig = ToolsConfig(),
|
val tools: ToolsConfig = ToolsConfig(),
|
||||||
val providers: List<ProviderConfig> = emptyList(),
|
val providers: List<ProviderConfig> = emptyList(),
|
||||||
|
val router: RouterConfig = RouterConfig(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -55,3 +56,27 @@ data class ProviderConfig(
|
|||||||
val url: String = "http://127.0.0.1:10000",
|
val url: String = "http://127.0.0.1:10000",
|
||||||
val capabilities: Map<String, Double> = emptyMap(),
|
val capabilities: Map<String, Double> = emptyMap(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RouterConfig(
|
||||||
|
val embedder: EmbedderConfig = EmbedderConfig(),
|
||||||
|
val l3: L3Config = L3Config(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class EmbedderConfig(
|
||||||
|
val backend: String = "noop",
|
||||||
|
val dimension: Int = 1536,
|
||||||
|
val url: String? = null,
|
||||||
|
val modelId: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class L3Config(
|
||||||
|
val backend: String = "in_memory",
|
||||||
|
val persistPath: String? = null,
|
||||||
|
val pythonExecutable: String = "python3",
|
||||||
|
val scriptPath: String? = null,
|
||||||
|
val dim: Int = 1536,
|
||||||
|
val bitWidth: Int = 4,
|
||||||
|
)
|
||||||
|
|||||||
@@ -217,4 +217,62 @@ class ConfigLoaderTest {
|
|||||||
assertEquals(1.0, result.providers[0].capabilities["General"])
|
assertEquals(1.0, result.providers[0].capabilities["General"])
|
||||||
assertEquals(0.7, result.providers[0].capabilities["Coding"])
|
assertEquals(0.7, result.providers[0].capabilities["Coding"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseToml parses router embedder and l3 config`() {
|
||||||
|
val toml = """
|
||||||
|
[router.embedder]
|
||||||
|
backend = "llamacpp"
|
||||||
|
dimension = 1536
|
||||||
|
url = "http://localhost:11000"
|
||||||
|
model_id = "nomic-embed-text"
|
||||||
|
|
||||||
|
[router.l3]
|
||||||
|
backend = "turbovec"
|
||||||
|
persist_path = "/tmp/router/l3.tq"
|
||||||
|
python_executable = "python3"
|
||||||
|
dim = 1536
|
||||||
|
bit_width = 4
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val loader = ConfigLoader::class.java
|
||||||
|
val parseTomlMethod = loader.getDeclaredMethod("parseToml", String::class.java)
|
||||||
|
parseTomlMethod.isAccessible = true
|
||||||
|
val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig
|
||||||
|
|
||||||
|
assertEquals("llamacpp", result.router.embedder.backend)
|
||||||
|
assertEquals(1536, result.router.embedder.dimension)
|
||||||
|
assertEquals("http://localhost:11000", result.router.embedder.url)
|
||||||
|
assertEquals("nomic-embed-text", result.router.embedder.modelId)
|
||||||
|
|
||||||
|
assertEquals("turbovec", result.router.l3.backend)
|
||||||
|
assertEquals("/tmp/router/l3.tq", result.router.l3.persistPath)
|
||||||
|
assertEquals("python3", result.router.l3.pythonExecutable)
|
||||||
|
assertEquals(1536, result.router.l3.dim)
|
||||||
|
assertEquals(4, result.router.l3.bitWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseToml applies router defaults when sections absent`() {
|
||||||
|
val toml = """
|
||||||
|
[server]
|
||||||
|
host = "localhost"
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val loader = ConfigLoader::class.java
|
||||||
|
val parseTomlMethod = loader.getDeclaredMethod("parseToml", String::class.java)
|
||||||
|
parseTomlMethod.isAccessible = true
|
||||||
|
val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig
|
||||||
|
|
||||||
|
assertEquals("noop", result.router.embedder.backend)
|
||||||
|
assertEquals(1536, result.router.embedder.dimension)
|
||||||
|
assertEquals(null, result.router.embedder.url)
|
||||||
|
assertEquals(null, result.router.embedder.modelId)
|
||||||
|
|
||||||
|
assertEquals("in_memory", result.router.l3.backend)
|
||||||
|
assertEquals(null, result.router.l3.persistPath)
|
||||||
|
assertEquals("python3", result.router.l3.pythonExecutable)
|
||||||
|
assertEquals(1536, result.router.l3.dim)
|
||||||
|
assertEquals(4, result.router.l3.bitWidth)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,3 +50,18 @@ capabilities = { General = 1.0, Coding = 0.7, Reasoning = 0.6, Summarization = 0
|
|||||||
# model_path = "~/models/neural-chat-7b-gguf/model.gguf"
|
# model_path = "~/models/neural-chat-7b-gguf/model.gguf"
|
||||||
# url = "http://127.0.0.1:10001"
|
# url = "http://127.0.0.1:10001"
|
||||||
# capabilities = { General = 0.9, Coding = 0.8, Reasoning = 0.7, Summarization = 0.75, ToolCalling = 0.6 }
|
# capabilities = { General = 0.9, Coding = 0.8, Reasoning = 0.7, Summarization = 0.75, ToolCalling = 0.6 }
|
||||||
|
|
||||||
|
# Router configuration (optional, defaults shown below)
|
||||||
|
[router.embedder]
|
||||||
|
backend = "noop" # or "llamacpp"
|
||||||
|
dimension = 1536
|
||||||
|
# url = "http://127.0.0.1:11000"
|
||||||
|
# model_id = "nomic-embed-text"
|
||||||
|
|
||||||
|
[router.l3]
|
||||||
|
backend = "in_memory" # or "turbovec"
|
||||||
|
# persist_path = "~/.config/correx/router/l3/index.tq"
|
||||||
|
# python_executable = "python3"
|
||||||
|
# script_path = "~/.config/correx/python/turbovec_sidecar.py"
|
||||||
|
# dim = 1536
|
||||||
|
# bit_width = 4
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ dependencies {
|
|||||||
implementation project(":core:inference")
|
implementation project(":core:inference")
|
||||||
implementation project(":core:approvals")
|
implementation project(":core:approvals")
|
||||||
implementation project(":core:sessions")
|
implementation project(":core:sessions")
|
||||||
|
implementation project(":core:config")
|
||||||
implementation project(":infrastructure:inference")
|
implementation project(":infrastructure:inference")
|
||||||
implementation project(":infrastructure:inference:commons")
|
implementation project(":infrastructure:inference:commons")
|
||||||
implementation project(":infrastructure:inference:llama_cpp")
|
implementation project(":infrastructure:inference:llama_cpp")
|
||||||
implementation project(":infrastructure:persistence")
|
implementation project(":infrastructure:persistence")
|
||||||
|
implementation project(":infrastructure:router:turbovec")
|
||||||
implementation project(":infrastructure:tools")
|
implementation project(":infrastructure:tools")
|
||||||
implementation project(":infrastructure:tools:filesystem")
|
implementation project(":infrastructure:tools:filesystem")
|
||||||
implementation project(":infrastructure:workflow")
|
implementation project(":infrastructure:workflow")
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ dependencies {
|
|||||||
implementation "io.ktor:ktor-client-content-negotiation:$ktor_version"
|
implementation "io.ktor:ktor-client-content-negotiation:$ktor_version"
|
||||||
implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"
|
implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"
|
||||||
testImplementation "org.junit.jupiter:junit-jupiter"
|
testImplementation "org.junit.jupiter:junit-jupiter"
|
||||||
|
testImplementation "io.ktor:ktor-client-mock:$ktor_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named("koverVerify").configure { enabled = false }
|
tasks.named("koverVerify").configure { enabled = false }
|
||||||
|
|||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
package com.correx.infrastructure.inference.llama.cpp
|
||||||
|
|
||||||
|
import com.correx.core.inference.Embedder
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.engine.cio.CIO
|
||||||
|
import io.ktor.client.plugins.HttpTimeout
|
||||||
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
|
import io.ktor.client.request.accept
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.contentType
|
||||||
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
private const val DEFAULT_REQUEST_TIMEOUT_MS = 60_000L
|
||||||
|
|
||||||
|
private fun defaultHttpClient(): HttpClient = HttpClient(CIO) {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(
|
||||||
|
Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
isLenient = true
|
||||||
|
encodeDefaults = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
install(HttpTimeout) {
|
||||||
|
requestTimeoutMillis = DEFAULT_REQUEST_TIMEOUT_MS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val json = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
explicitNulls = false
|
||||||
|
encodeDefaults = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger(LlamaCppEmbedder::class.java)
|
||||||
|
|
||||||
|
class LlamaCppEmbedder(
|
||||||
|
private val url: String,
|
||||||
|
private val modelId: String? = null,
|
||||||
|
override val dimension: Int = 1536,
|
||||||
|
private val httpClient: HttpClient = defaultHttpClient(),
|
||||||
|
) : Embedder {
|
||||||
|
|
||||||
|
override suspend fun embed(text: String): FloatArray {
|
||||||
|
val request = EmbeddingRequest(
|
||||||
|
content = text,
|
||||||
|
model = modelId,
|
||||||
|
)
|
||||||
|
|
||||||
|
val encoded = json.encodeToString(request)
|
||||||
|
log.debug("sending embedding request to llama.cpp: {}", encoded)
|
||||||
|
|
||||||
|
val responseBody = try {
|
||||||
|
httpClient.post("$url/embedding") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
accept(ContentType.Application.Json)
|
||||||
|
setBody(encoded)
|
||||||
|
}.body<String>()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IllegalStateException("Failed to call embedding endpoint at $url/embedding: ${e.message}", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("got embedding response from llama.cpp: {}", responseBody)
|
||||||
|
|
||||||
|
val embedding = parseEmbeddingResponse(responseBody)
|
||||||
|
|
||||||
|
if (embedding.size != dimension) {
|
||||||
|
throw IllegalStateException(
|
||||||
|
"Embedding dimension mismatch: expected $dimension but got ${embedding.size}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return embedding.toFloatArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseEmbeddingResponse(responseBody: String): List<Float> {
|
||||||
|
// Try OpenAI-compatible format first: {"data": [{"embedding": [...], "index": 0}]}
|
||||||
|
val openAiResult = runCatching {
|
||||||
|
val openAiResponse = json.decodeFromString<OpenAiEmbeddingResponse>(responseBody)
|
||||||
|
if (openAiResponse.data.isNotEmpty()) {
|
||||||
|
openAiResponse.data.first().embedding
|
||||||
|
} else {
|
||||||
|
emptyList<Float>()
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
if (openAiResult != null && openAiResult.isNotEmpty()) {
|
||||||
|
return openAiResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try simple format: {"embedding": [...]}
|
||||||
|
val simpleResult = runCatching {
|
||||||
|
val simpleResponse = json.decodeFromString<EmbeddingResponse>(responseBody)
|
||||||
|
simpleResponse.embedding ?: emptyList<Float>()
|
||||||
|
}.getOrNull()
|
||||||
|
if (simpleResult != null && simpleResult.isNotEmpty()) {
|
||||||
|
return simpleResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try array format: [{"embedding": [...]}]
|
||||||
|
val arrayResult = runCatching {
|
||||||
|
val arrayResponse = json.decodeFromString<List<EmbeddingArrayResponse>>(responseBody)
|
||||||
|
if (arrayResponse.isNotEmpty()) {
|
||||||
|
arrayResponse.first().embedding
|
||||||
|
} else {
|
||||||
|
emptyList<Float>()
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
if (arrayResult != null && arrayResult.isNotEmpty()) {
|
||||||
|
return arrayResult
|
||||||
|
}
|
||||||
|
|
||||||
|
throw IllegalStateException(
|
||||||
|
"Unexpected embedding response shape from $url. " +
|
||||||
|
"Response body (first 200 chars): ${responseBody.take(200)}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package com.correx.infrastructure.inference.llama.cpp
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class EmbeddingRequest(
|
||||||
|
val content: String,
|
||||||
|
val model: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class EmbeddingResponse(
|
||||||
|
val embedding: List<Float>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class EmbeddingArrayResponse(
|
||||||
|
val embedding: List<Float>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class OpenAiEmbeddingData(
|
||||||
|
val embedding: List<Float>,
|
||||||
|
val index: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class OpenAiEmbeddingResponse(
|
||||||
|
val data: List<OpenAiEmbeddingData>,
|
||||||
|
)
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
package com.correx.infrastructure.inference.llama.cpp
|
||||||
|
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.engine.mock.MockEngine
|
||||||
|
import io.ktor.client.engine.mock.respond
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.HttpHeaders
|
||||||
|
import io.ktor.http.headersOf
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.assertThrows
|
||||||
|
|
||||||
|
class LlamaCppEmbedderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `embed returns float array with correct dimension from OpenAI format`(): Unit = runBlocking {
|
||||||
|
val mockEngine = MockEngine { request ->
|
||||||
|
val responseBody = """
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"embedding": [0.1, 0.2, 0.3],
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
respond(
|
||||||
|
content = responseBody,
|
||||||
|
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val embeddingUrl = "http://localhost:11000"
|
||||||
|
val httpClient = HttpClient(mockEngine)
|
||||||
|
val embedder = LlamaCppEmbedder(
|
||||||
|
url = embeddingUrl,
|
||||||
|
modelId = "test-model",
|
||||||
|
dimension = 3,
|
||||||
|
httpClient = httpClient,
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = embedder.embed("test text")
|
||||||
|
|
||||||
|
assert(result.size == 3)
|
||||||
|
assert(kotlin.math.abs(result[0] - 0.1f) < 0.0001f)
|
||||||
|
assert(kotlin.math.abs(result[1] - 0.2f) < 0.0001f)
|
||||||
|
assert(kotlin.math.abs(result[2] - 0.3f) < 0.0001f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `embed returns float array from simple embedding response format`(): Unit = runBlocking {
|
||||||
|
val mockEngine = MockEngine { request ->
|
||||||
|
val responseBody = """
|
||||||
|
{
|
||||||
|
"embedding": [0.5, 0.6, 0.7]
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
respond(
|
||||||
|
content = responseBody,
|
||||||
|
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val embeddingUrl = "http://localhost:11000"
|
||||||
|
val httpClient = HttpClient(mockEngine)
|
||||||
|
val embedder = LlamaCppEmbedder(
|
||||||
|
url = embeddingUrl,
|
||||||
|
modelId = null,
|
||||||
|
dimension = 3,
|
||||||
|
httpClient = httpClient,
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = embedder.embed("test text")
|
||||||
|
|
||||||
|
assert(result.size == 3)
|
||||||
|
assert(kotlin.math.abs(result[0] - 0.5f) < 0.0001f)
|
||||||
|
assert(kotlin.math.abs(result[1] - 0.6f) < 0.0001f)
|
||||||
|
assert(kotlin.math.abs(result[2] - 0.7f) < 0.0001f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `embed throws on dimension mismatch`(): Unit = runBlocking {
|
||||||
|
val mockEngine = MockEngine { request ->
|
||||||
|
val responseBody = """
|
||||||
|
{
|
||||||
|
"embedding": [0.1, 0.2, 0.3, 0.4]
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
respond(
|
||||||
|
content = responseBody,
|
||||||
|
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val embeddingUrl = "http://localhost:11000"
|
||||||
|
val httpClient = HttpClient(mockEngine)
|
||||||
|
val embedder = LlamaCppEmbedder(
|
||||||
|
url = embeddingUrl,
|
||||||
|
dimension = 3,
|
||||||
|
httpClient = httpClient,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThrows<IllegalStateException> {
|
||||||
|
runBlocking {
|
||||||
|
embedder.embed("test text")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `embed throws on unexpected response format`(): Unit = runBlocking {
|
||||||
|
val mockEngine = MockEngine { request ->
|
||||||
|
val responseBody = """
|
||||||
|
{
|
||||||
|
"unexpected_field": "value"
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
respond(
|
||||||
|
content = responseBody,
|
||||||
|
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val embeddingUrl = "http://localhost:11000"
|
||||||
|
val httpClient = HttpClient(mockEngine)
|
||||||
|
val embedder = LlamaCppEmbedder(
|
||||||
|
url = embeddingUrl,
|
||||||
|
dimension = 3,
|
||||||
|
httpClient = httpClient,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThrows<IllegalStateException> {
|
||||||
|
runBlocking {
|
||||||
|
embedder.embed("test text")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import com.correx.core.approvals.DefaultApprovalRepository
|
|||||||
import com.correx.core.artifacts.DefaultArtifactReducer
|
import com.correx.core.artifacts.DefaultArtifactReducer
|
||||||
import com.correx.core.artifacts.repository.ArtifactRepository
|
import com.correx.core.artifacts.repository.ArtifactRepository
|
||||||
import com.correx.core.artifactstore.ArtifactStore
|
import com.correx.core.artifactstore.ArtifactStore
|
||||||
|
import com.correx.core.config.EmbedderConfig
|
||||||
|
import com.correx.core.config.L3Config
|
||||||
import com.correx.core.events.EventDispatcher
|
import com.correx.core.events.EventDispatcher
|
||||||
import com.correx.core.events.stores.EventStore
|
import com.correx.core.events.stores.EventStore
|
||||||
import com.correx.core.inference.CapabilityScore
|
import com.correx.core.inference.CapabilityScore
|
||||||
@@ -33,7 +35,10 @@ import com.correx.infrastructure.artifactscas.index.SqliteArtifactIndex
|
|||||||
import com.correx.infrastructure.inference.DefaultProviderRegistry
|
import com.correx.infrastructure.inference.DefaultProviderRegistry
|
||||||
import com.correx.infrastructure.inference.commons.ModelDescriptor
|
import com.correx.infrastructure.inference.commons.ModelDescriptor
|
||||||
import com.correx.infrastructure.inference.commons.ResidencyMode
|
import com.correx.infrastructure.inference.commons.ResidencyMode
|
||||||
|
import com.correx.infrastructure.inference.llama.cpp.LlamaCppEmbedder
|
||||||
import com.correx.infrastructure.inference.llama.cpp.LlamaCppInferenceProvider
|
import com.correx.infrastructure.inference.llama.cpp.LlamaCppInferenceProvider
|
||||||
|
import com.correx.infrastructure.router.turbovec.TurboVecL3MemoryStore
|
||||||
|
import com.correx.infrastructure.router.turbovec.TurboVecSidecarConfig
|
||||||
import com.correx.infrastructure.persistence.SqliteEventStore
|
import com.correx.infrastructure.persistence.SqliteEventStore
|
||||||
import com.correx.infrastructure.persistence.artifact.LiveArtifactRepository
|
import com.correx.infrastructure.persistence.artifact.LiveArtifactRepository
|
||||||
import com.correx.infrastructure.tools.DefaultToolRegistry
|
import com.correx.infrastructure.tools.DefaultToolRegistry
|
||||||
@@ -49,6 +54,7 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
import java.sql.DriverManager
|
import java.sql.DriverManager
|
||||||
|
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
@@ -137,6 +143,70 @@ object InfrastructureModule {
|
|||||||
|
|
||||||
fun createInMemoryL3MemoryStore(): L3MemoryStore = InMemoryL3MemoryStore()
|
fun createInMemoryL3MemoryStore(): L3MemoryStore = InMemoryL3MemoryStore()
|
||||||
|
|
||||||
|
fun createLlamaCppEmbedder(config: EmbedderConfig): Embedder {
|
||||||
|
val url = config.url ?: throw IllegalArgumentException(
|
||||||
|
"EmbedderConfig with backend 'llamacpp' requires non-null 'url'"
|
||||||
|
)
|
||||||
|
return LlamaCppEmbedder(
|
||||||
|
url = url,
|
||||||
|
modelId = config.modelId,
|
||||||
|
dimension = config.dimension,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createTurboVecL3MemoryStore(config: L3Config): L3MemoryStore {
|
||||||
|
val scriptPath = config.scriptPath?.let { Paths.get(it) }
|
||||||
|
?: extractBundledTurboVecScript()
|
||||||
|
|
||||||
|
val persistPath = config.persistPath?.let { Paths.get(it) }
|
||||||
|
|
||||||
|
val sidecarConfig = TurboVecSidecarConfig(
|
||||||
|
pythonExecutable = config.pythonExecutable,
|
||||||
|
scriptPath = scriptPath,
|
||||||
|
dim = config.dim,
|
||||||
|
bitWidth = config.bitWidth,
|
||||||
|
persistPath = persistPath,
|
||||||
|
)
|
||||||
|
|
||||||
|
return TurboVecL3MemoryStore(sidecarConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractBundledTurboVecScript(): Path {
|
||||||
|
val cacheDir = Paths.get(System.getProperty("user.home"), ".cache", "correx")
|
||||||
|
Files.createDirectories(cacheDir)
|
||||||
|
|
||||||
|
val targetPath = cacheDir.resolve("turbovec_sidecar.py")
|
||||||
|
|
||||||
|
val inputStream = InfrastructureModule::class.java.getResourceAsStream("/python/turbovec_sidecar.py")
|
||||||
|
?: throw IllegalStateException("Could not find bundled turbovec_sidecar.py in classpath")
|
||||||
|
|
||||||
|
inputStream.use { input ->
|
||||||
|
Files.copy(input, targetPath, StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetPath
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createEmbedderFromConfig(config: EmbedderConfig): Embedder {
|
||||||
|
return when (config.backend) {
|
||||||
|
"noop" -> createNoopEmbedder(config.dimension)
|
||||||
|
"llamacpp" -> createLlamaCppEmbedder(config)
|
||||||
|
else -> throw IllegalArgumentException(
|
||||||
|
"Unknown embedder backend: '${config.backend}'. Supported: noop, llamacpp"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createL3MemoryStoreFromConfig(config: L3Config): L3MemoryStore {
|
||||||
|
return when (config.backend) {
|
||||||
|
"in_memory" -> createInMemoryL3MemoryStore()
|
||||||
|
"turbovec" -> createTurboVecL3MemoryStore(config)
|
||||||
|
else -> throw IllegalArgumentException(
|
||||||
|
"Unknown L3 backend: '${config.backend}'. Supported: in_memory, turbovec"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun createRouterFacade(
|
fun createRouterFacade(
|
||||||
eventStore: EventStore,
|
eventStore: EventStore,
|
||||||
inferenceRouter: InferenceRouter,
|
inferenceRouter: InferenceRouter,
|
||||||
|
|||||||
Reference in New Issue
Block a user