feat(memory): migrate EmbeddingProvider to KMP-compatible TextEmbeddingExecutor, add :memory-md-vector, and hybrid backend support
- Replaced `EmbeddingProvider` with cross-platform `TextEmbeddingExecutor` for native target compatibility. - Introduced `:memory-md-vector` module combining vector-cache and `.md` file-based memory systems (`hybrid` backend). - Updated `SiglipEmbeddingProvider` to use KMP `TextEmbeddingExtractor` and streamlined compatibility via `asExecutor`. - Added hybrid memory backend to `standalone`, supporting `.md` reconciliation with vector-cache for semantic
This commit is contained in:
@@ -57,6 +57,9 @@ kotlin {
|
||||
implementation(project(":memory-md"))
|
||||
if (!skipVectorMemory) {
|
||||
implementation(project(":memory-vector"))
|
||||
// Гибрид: .md (single source of truth) + ksqlite vector-кэш
|
||||
// (sqlite-vec vec0). Подключается при AGENTIK_MEMORY_BACKEND=hybrid.
|
||||
implementation(project(":memory-md-vector"))
|
||||
}
|
||||
|
||||
// SQLite-бэкенд (ksqlite JNI) — JVM-only по факту.
|
||||
@@ -166,3 +169,42 @@ val shadowJarTask = tasks.register<ShadowJar>("shadowJar") {
|
||||
|
||||
includeEmptyDirs = false
|
||||
}
|
||||
|
||||
// :standalone deploy
|
||||
//
|
||||
// `deployJar` собирает fatjar через shadowJar и копирует его на приватный
|
||||
// домашний хост (root@192.168.76.166). Репа приватная, IP захардкожен —
|
||||
// пользователь явно сказал "можно в наглую указать конкретно этот ip".
|
||||
// Если понадобится другой хост — переопредели через -PagentikDeployHost=...
|
||||
//
|
||||
// Конвенция имени файла: agentik-standalone-{version}-all.jar (как на хосте
|
||||
// уже лежат agentik-cli-0.1.0-all.jar / agentik-0.1.0-all.jar —
|
||||
// выровнено по стилю существующего деплоя).
|
||||
//
|
||||
// SSH — ключевой, пароль не спрашивает. Если упадёт — проверь
|
||||
// `ssh root@192.168.76.166 'echo OK'` вручную.
|
||||
val agentikDeployHost: String =
|
||||
(project.findProperty("agentikDeployHost") as? String) ?: "root@192.168.76.166"
|
||||
val agentikDeployPath: String =
|
||||
(project.findProperty("agentikDeployPath") as? String) ?: "~/agentik-standalone-${project.version}-all.jar"
|
||||
|
||||
tasks.register("deployJar") {
|
||||
group = "deployment"
|
||||
description = "Builds shadowJar and copies it to $agentikDeployHost:$agentikDeployPath"
|
||||
dependsOn(shadowJarTask)
|
||||
|
||||
doLast {
|
||||
val jar = shadowJarTask.get().outputs.files.singleFile
|
||||
val target = "$agentikDeployHost:$agentikDeployPath"
|
||||
println("→ scp $jar $target")
|
||||
val proc = ProcessBuilder("scp", jar.absolutePath, target)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
proc.inputStream.bufferedReader().forEachLine { println("scp> $it") }
|
||||
val rc = proc.waitFor()
|
||||
if (rc != 0) {
|
||||
throw GradleException("scp failed (exit=$rc). Check ssh access to $agentikDeployHost.")
|
||||
}
|
||||
println("✓ deployed to $target")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import pw.binom.a2a.server.a2aAgent
|
||||
import pw.binom.agentik.memory.MemoryReviewer
|
||||
import pw.binom.agentik.memory.MemorySystem
|
||||
import pw.binom.agentik.memory.md.openMdMemorySystem
|
||||
import pw.binom.agentik.memory.mdvector.openHybridMemorySystem
|
||||
import pw.binom.agentik.memory.vector.VectorMemorySystem
|
||||
import pw.binom.agentik.memory.vector.embedding.HttpEmbeddingClient
|
||||
import pw.binom.agentik.memory.vector.embedding.SiglipEmbeddingProvider
|
||||
@@ -256,7 +257,7 @@ private fun runServer() {
|
||||
}
|
||||
}
|
||||
MemoryBackend.VECTOR -> {
|
||||
val embedding: pw.binom.agentik.memory.vector.EmbeddingProvider = when (config.embedding.backend) {
|
||||
val embedding: pw.binom.agentik.memory.TextEmbeddingExecutor = when (config.embedding.backend) {
|
||||
AppConfig.EmbeddingBackend.HTTP -> {
|
||||
val llm = config.llm
|
||||
// Берём базовый URL + API key у активного LLM-бэкенда.
|
||||
@@ -271,7 +272,7 @@ private fun runServer() {
|
||||
apiKey = oa.apiKey,
|
||||
model = config.embedding.model,
|
||||
dimension = config.embedding.dimension,
|
||||
)
|
||||
).asExecutor()
|
||||
}
|
||||
AppConfig.EmbeddingBackend.SIGLIP -> {
|
||||
val modelPath = checkNotNull(config.embedding.modelPath) {
|
||||
@@ -280,7 +281,7 @@ private fun runServer() {
|
||||
val tokenizerPath = checkNotNull(config.embedding.tokenizerPath) {
|
||||
"AGENTIK_EMBEDDING_BACKEND=siglip требует AGENTIK_EMBEDDING_TOKENIZER_PATH"
|
||||
}
|
||||
SiglipEmbeddingProvider(modelPath = modelPath, tokenizerPath = tokenizerPath)
|
||||
SiglipEmbeddingProvider(modelPath = modelPath, tokenizerPath = tokenizerPath).asExecutor()
|
||||
}
|
||||
}
|
||||
VectorMemorySystem.open(
|
||||
@@ -296,6 +297,44 @@ private fun runServer() {
|
||||
println(" memory: db=${config.agent.dbPath} (vector-backend, $backendLabel)")
|
||||
}
|
||||
}
|
||||
MemoryBackend.HYBRID -> {
|
||||
// Гибрид: .md файлы (single source of truth) + ksqlite vector-кэш.
|
||||
// Нужны embeddings (HTTP/SIGLIP) + путь к vector-БД (по умолчанию рядом с agentik.db).
|
||||
val embedding: pw.binom.agentik.memory.TextEmbeddingExecutor = when (config.embedding.backend) {
|
||||
AppConfig.EmbeddingBackend.HTTP -> {
|
||||
val llm = config.llm
|
||||
require(llm.backend == LlmBackend.OPENAI) {
|
||||
"AGENTIK_EMBEDDING_BACKEND=http требует LLM_BACKEND=openai"
|
||||
}
|
||||
val oa = checkNotNull(llm.openai) { "openai config required" }
|
||||
HttpEmbeddingClient(
|
||||
apiUrl = oa.baseUrl.trimEnd('/'),
|
||||
apiKey = oa.apiKey,
|
||||
model = config.embedding.model,
|
||||
dimension = config.embedding.dimension,
|
||||
).asExecutor()
|
||||
}
|
||||
AppConfig.EmbeddingBackend.SIGLIP -> {
|
||||
val modelPath = checkNotNull(config.embedding.modelPath) {
|
||||
"AGENTIK_EMBEDDING_BACKEND=siglip требует AGENTIK_EMBEDDING_MODEL_PATH"
|
||||
}
|
||||
val tokenizerPath = checkNotNull(config.embedding.tokenizerPath) {
|
||||
"AGENTIK_EMBEDDING_BACKEND=siglip требует AGENTIK_EMBEDDING_TOKENIZER_PATH"
|
||||
}
|
||||
SiglipEmbeddingProvider(modelPath = modelPath, tokenizerPath = tokenizerPath).asExecutor()
|
||||
}
|
||||
}
|
||||
val mdDir = rawMemory.takeUnless { it.equals("off", true) } ?: defaultMemoryDir()
|
||||
val vectorDbPath = deriveVectorDbPath(config.agent.dbPath)
|
||||
openHybridMemorySystem(
|
||||
memoryRoot = Path(mdDir),
|
||||
vectorDbPath = Path(vectorDbPath),
|
||||
dimension = embedding.dimension,
|
||||
embedder = embedding,
|
||||
).also {
|
||||
println(" memory: md=$mdDir, vector-cache=$vectorDbPath (hybrid-backend, dim=${embedding.dimension})")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Контекстное окно модели (для compaction'а working memory).
|
||||
@@ -428,3 +467,12 @@ private fun defaultMemoryDir(): String {
|
||||
val home = System.getProperty("user.home") ?: "."
|
||||
return "$home/.agentik/memory"
|
||||
}
|
||||
|
||||
/**
|
||||
* Производный путь к vector-БД гибридного бэкенда: рядом с `agentik.db`,
|
||||
* но с суффиксом `-vectors`. Например: `agentik.db` → `agentik-vectors.db`.
|
||||
*/
|
||||
private fun deriveVectorDbPath(agentDbPath: String): String {
|
||||
val withoutExt = agentDbPath.removeSuffix(".db")
|
||||
return "$withoutExt-vectors.db"
|
||||
}
|
||||
|
||||
@@ -115,9 +115,29 @@ data class AppConfig(
|
||||
val endpoints: Boolean = false,
|
||||
)
|
||||
|
||||
/** Бэкенд долговременной памяти. */
|
||||
/** Бэкенд долговременной памяти.
|
||||
*
|
||||
* Env-алиасы (для удобства CLI):
|
||||
* - `md` → [MD] (Hermes-style §-файлы)
|
||||
* - `vector` → [VECTOR] (sqlite + JVector + LLM-embeddings)
|
||||
* - `md-vector` → [HYBRID] (md-файлы = source of truth + sqlite vector-кэш)
|
||||
* - `off` → [OFF] (память выключена)
|
||||
*/
|
||||
@Serializable
|
||||
enum class MemoryBackend { MD, VECTOR, OFF }
|
||||
enum class MemoryBackend {
|
||||
MD, VECTOR, HYBRID, OFF;
|
||||
|
||||
companion object {
|
||||
/** Маппинг env-строки (lowercase, как приходит из shell) → enum. */
|
||||
fun fromEnvString(s: String): MemoryBackend? = when (s.lowercase()) {
|
||||
"md" -> MD
|
||||
"vector" -> VECTOR
|
||||
"md-vector", "hybrid" -> HYBRID
|
||||
"off", "none" -> OFF
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Бэкенд эмбеддингов (для memory-backend=vector). */
|
||||
@Serializable
|
||||
@@ -183,7 +203,7 @@ data class AppConfig(
|
||||
memory = MemorySection(
|
||||
dir = env("AGENTIK_MEMORY_DIR")?.takeIf { it.isNotBlank() },
|
||||
backend = env("AGENTIK_MEMORY_BACKEND")?.let {
|
||||
runCatching { MemoryBackend.valueOf(it.uppercase()) }.getOrNull()
|
||||
MemoryBackend.fromEnvString(it)
|
||||
} ?: MemoryBackend.MD,
|
||||
compressionThreshold = env("AGENTIK_COMPRESSION_THRESHOLD")?.toDoubleOrNull()
|
||||
?.coerceIn(0.1, 0.99) ?: DEFAULT_COMPRESSION_THRESHOLD,
|
||||
|
||||
Reference in New Issue
Block a user