memory: SigLIP2 on-device embedding (text-embedding-kmp v3)
Добавляет второй бэкенд эмбеддингов для vector-памяти: on-device SigLIP2
через ONNX Runtime. Не требует сети (HTTP), не светят тексты заметок наружу.
* gradle/libs.versions.toml: text-embedding-kmp = "3.0.0-SNAPSHOT", модули
api-jvm / siglip-jvm (group переехал с pw.binom.voice.embeddingtext на
pw.binom.ai.embeddingtext).
* settings.gradle.kts: mavenLocal() добавлен в dependencyResolutionManagement
(text-embedding-kmp публикуется локально как snapshot).
* memory-vector/SiglipEmbeddingProvider (jvmMain) — адаптер
pw.binom.voice.embeddingtext.TextEmbeddingExtractor → EmbeddingProvider:
оборачивает blocking embed() в withContext(Dispatchers.IO) + Mutex (ONNX
сессия не reentrant), размерность пробируется через probe embed("probe")
(768 для SigLIP2-base).
* memory-vector/build.gradle.kts: api(libs.text.embedding.api) +
implementation(libs.text.embedding.siglip); jvmTest получает
SiglipEmbeddingProviderTest — smoke test (skip если модель не найдена).
* standalone/AgentikConfig: новый enum EmbeddingBackend { HTTP, SIGLIP },
поля embeddingBackend / embeddingModelPath / embeddingTokenizerPath, env:
AGENTIK_EMBEDDING_BACKEND, AGENTIK_EMBEDDING_MODEL_PATH,
AGENTIK_EMBEDDING_TOKENIZER_PATH.
* standalone/Main.kt: switch на embeddingBackend при memory-backend=vector;
для SIGLIP требуются оба пути, иначе ошибка с понятным сообщением.
* standalone/README.md: обновлены env-vars, добавлены две bash-секции
(HTTP и SIGLIP) + инструкция скачивания модели с static.binom.pw.
* scripts/install-text-embedding-stub.sh: workaround для upstream бага
(siglip-jvm/*.module ссылается на api без -jvm variant). Создаёт
stub-артефакт api:3.0.0-SNAPSHOT в mavenLocal с тем же содержимым.
Удалить когда upstream починит module-metadata.
Smoke test: AGENTIK_MEMORY_BACKEND=vector AGENTIK_EMBEDDING_BACKEND=siglip
+ несуществующий путь → FileNotFoundException с понятным трейсом (значит
ONNX Runtime инициализирован, путь через factory пробрасывается корректно).
Tests: 288 total green. Fatjar 250MB (вырос из-за onnxruntime ~80MB).
Dropped: старый stub-jar pw.binom.voice.embeddingtext:api:2.0.0-SNAPSHOT.
This commit is contained in:
@@ -10,6 +10,7 @@ litert = "7"
|
|||||||
sqldelight = "2.3.2"
|
sqldelight = "2.3.2"
|
||||||
shadow = "8.3.5"
|
shadow = "8.3.5"
|
||||||
jvector = "3.0.6"
|
jvector = "3.0.6"
|
||||||
|
text-embedding-kmp = "3.0.0-SNAPSHOT"
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
||||||
@@ -67,3 +68,8 @@ kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.re
|
|||||||
|
|
||||||
# --- JVector (io.github.jbellis) — embedded ANN-индекс для vector-бэкенда памяти. JVM-only. ---
|
# --- JVector (io.github.jbellis) — embedded ANN-индекс для vector-бэкенда памяти. JVM-only. ---
|
||||||
jvector = { module = "io.github.jbellis:jvector", version.ref = "jvector" }
|
jvector = { module = "io.github.jbellis:jvector", version.ref = "jvector" }
|
||||||
|
|
||||||
|
# --- text-embedding-kmp (pw.binom.ai.embeddingtext) — on-device SigLIP2 эмбеддинг через ONNX. ---
|
||||||
|
# Артефакты публикуются под именами `-jvm` (KMP convention для JVM-таргета).
|
||||||
|
text-embedding-api = { module = "pw.binom.ai.embeddingtext:api-jvm", version.ref = "text-embedding-kmp" }
|
||||||
|
text-embedding-siglip = { module = "pw.binom.ai.embeddingtext:siglip-jvm", version.ref = "text-embedding-kmp" }
|
||||||
|
|||||||
@@ -24,6 +24,23 @@ kotlin {
|
|||||||
jvmMain.dependencies {
|
jvmMain.dependencies {
|
||||||
implementation(libs.jvector)
|
implementation(libs.jvector)
|
||||||
implementation(libs.sqldelight.sqlite.driver)
|
implementation(libs.sqldelight.sqlite.driver)
|
||||||
|
// text-embedding-kmp — on-device SigLIP2 через ONNX Runtime.
|
||||||
|
// Сигнатура `embed(String): TextEmbedding` (blocking), оборачиваем
|
||||||
|
// наш `suspend fun embed(text)` через Mutex. api-вариант экспортируем
|
||||||
|
// (`api`), потому что SiglipEmbeddingProvider реализует `embed()`
|
||||||
|
// через тип TextEmbeddingExtractor, который виден потребителю
|
||||||
|
// только если он сам подтянет api-jvm — проще пробросить.
|
||||||
|
//
|
||||||
|
// WORKAROUND: upstream `siglip-jvm/*.module` ссылается на `api`
|
||||||
|
// БЕЗ -jvm суффикса. Поскольку в mavenLocal есть только `api-jvm`,
|
||||||
|
// требуется дополнительный stub-jar `pw.binom.ai.embeddingtext:api`
|
||||||
|
// с тем же содержимым. Создаётся так:
|
||||||
|
// mkdir -p ~/.m2/repository/pw.binom.ai.embeddingtext/api/3.0.0-SNAPSHOT
|
||||||
|
// cp ~/.m2/repository/.../api-jvm/3.0.0-SNAPSHOT/api-jvm-*.{jar,sources.jar} \
|
||||||
|
// ~/.m2/repository/.../api/3.0.0-SNAPSHOT/api-*.{jar,sources.jar}
|
||||||
|
// Когда upstream починит module-metadata — эту инструкцию можно убрать.
|
||||||
|
api(libs.text.embedding.api)
|
||||||
|
implementation(libs.text.embedding.siglip)
|
||||||
}
|
}
|
||||||
jvmTest.dependencies {
|
jvmTest.dependencies {
|
||||||
implementation(kotlin("test"))
|
implementation(kotlin("test"))
|
||||||
|
|||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
package pw.binom.agentik.memory.vector.embedding
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import pw.binom.agentik.memory.vector.EmbeddingProvider
|
||||||
|
import pw.binom.voice.embeddingtext.TextEmbeddingExtractor
|
||||||
|
import pw.binom.voice.embeddingtext.createSiglip2TextExtractor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Локальный on-device эмбеддинг через [TextEmbeddingExtractor] (SigLIP2 / ONNX).
|
||||||
|
*
|
||||||
|
* Особенности:
|
||||||
|
* - `TextEmbeddingExtractor.embed(text)` — **blocking** (ONNX-инференс на CPU),
|
||||||
|
* не suspend. Оборачиваем в `Dispatchers.IO` + `Mutex`, чтобы сериализовать
|
||||||
|
* доступ из нескольких корутин (ONNX-сессия не reentrant).
|
||||||
|
* - Размерность фиксирована extractor'ом (SigLIP2-base = 768); параметр
|
||||||
|
* `dimension` в конструкторе не принимаем — берём через [probeDimension].
|
||||||
|
* - LRU-кэш из [HttpEmbeddingClient] не используем здесь: ONNX-инференс на
|
||||||
|
* CPU ≈ 5-15 мс, кэш полезен только для HTTP. Но если потребуется —
|
||||||
|
* легко добавить.
|
||||||
|
*
|
||||||
|
* Модель + токенизатор не бандлятся в jar: передаём пути в конструкторе.
|
||||||
|
* Скачать: см. README репы `text-embedding-kmp`.
|
||||||
|
*/
|
||||||
|
class SiglipEmbeddingProvider(
|
||||||
|
modelPath: String,
|
||||||
|
tokenizerPath: String,
|
||||||
|
) : EmbeddingProvider, AutoCloseable {
|
||||||
|
|
||||||
|
private val extractor: TextEmbeddingExtractor =
|
||||||
|
createSiglip2TextExtractor(modelPath = modelPath, tokenizerPath = tokenizerPath)
|
||||||
|
|
||||||
|
override val dimension: Int = run {
|
||||||
|
val probe = extractor.embed("probe")
|
||||||
|
probe.dim
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
override suspend fun embed(text: String): FloatArray = withContext(Dispatchers.IO) {
|
||||||
|
mutex.withLock { extractor.embed(text).values }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
extractor.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
package pw.binom.agentik.memory.vector.embedding
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smoke-test SiglipEmbeddingProvider.
|
||||||
|
*
|
||||||
|
* Если файлы модели не найдены (по дефолту `/tmp/text-emb-model/`),
|
||||||
|
* тест пропускается через [assumeModelAvailable]. Если найдены —
|
||||||
|
* проверяется, что провайдер открывается, возвращает валидный эмбеддинг
|
||||||
|
* правильной размерности, и закрывается чисто.
|
||||||
|
*/
|
||||||
|
class SiglipEmbeddingProviderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dimension is 768 when model loads successfully`() {
|
||||||
|
val modelDir = File("/tmp/text-emb-model")
|
||||||
|
assume(modelDir.exists() && File(modelDir, "text_model_int8.onnx").exists()) {
|
||||||
|
"SigLIP2 model files not found in /tmp/text-emb-model/ — skipping"
|
||||||
|
}
|
||||||
|
SiglipEmbeddingProvider(
|
||||||
|
modelPath = "${modelDir.absolutePath}/text_model_int8.onnx",
|
||||||
|
tokenizerPath = "${modelDir.absolutePath}/tokenizer.model",
|
||||||
|
).use { provider ->
|
||||||
|
assertEquals(768, provider.dimension, "SigLIP2-base should produce 768-dim embeddings")
|
||||||
|
val v = kotlinx.coroutines.runBlocking { provider.embed("hello world") }
|
||||||
|
assertEquals(768, v.size)
|
||||||
|
assertTrue(v.any { it != 0f }, "embedding should not be all zeros")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `missing model file fails with clear error`() {
|
||||||
|
val tmpDir = createTempDir(prefix = "no-model-")
|
||||||
|
val nonExistent = File(tmpDir, "does-not-exist.onnx")
|
||||||
|
assertFailsWith<Exception> {
|
||||||
|
SiglipEmbeddingProvider(
|
||||||
|
modelPath = nonExistent.absolutePath,
|
||||||
|
tokenizerPath = nonExistent.absolutePath,
|
||||||
|
).use { it.dimension }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun assume(condition: Boolean, message: () -> String) {
|
||||||
|
if (!condition) {
|
||||||
|
println("SKIPPED: ${message()}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Создаёт stub-артефакт `pw.binom.voice.embeddingtext:api:2.0.0-SNAPSHOT`,
|
||||||
|
# ссылающийся на `api-jvm:2.0.0-SNAPSHOT`. Нужно из-за бага в upstream module
|
||||||
|
# metadata у siglip-jvm (ссылается на `api` без variant).
|
||||||
|
#
|
||||||
|
# Удалить, когда upstream починит module-metadata.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
M2="${HOME}/.m2/repository"
|
||||||
|
GROUP_DIR="${M2}/pw/binom/ai/embeddingtext"
|
||||||
|
SRC_DIR="${GROUP_DIR}/api-jvm/3.0.0-SNAPSHOT"
|
||||||
|
DST_DIR="${GROUP_DIR}/api/3.0.0-SNAPSHOT"
|
||||||
|
|
||||||
|
if [ ! -f "${SRC_DIR}/api-jvm-3.0.0-SNAPSHOT.jar" ]; then
|
||||||
|
echo "Source api-jvm not found: ${SRC_DIR}"
|
||||||
|
echo "Сначала опубликуй text-embedding-kmp в mavenLocal:"
|
||||||
|
echo " git clone https://git.binom.pw/subochev/text-embedding-kmp /tmp/text-embedding-kmp"
|
||||||
|
echo " cd /tmp/text-embedding-kmp && ./gradlew -Pversion=3.0.0-SNAPSHOT :api:publishJvmPublicationToMavenLocal :siglip:publishJvmPublicationToMavenLocal"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "${DST_DIR}"
|
||||||
|
cp "${SRC_DIR}/api-jvm-3.0.0-SNAPSHOT.jar" "${DST_DIR}/api-3.0.0-SNAPSHOT.jar"
|
||||||
|
cp "${SRC_DIR}/api-jvm-3.0.0-SNAPSHOT-sources.jar" "${DST_DIR}/api-3.0.0-SNAPSHOT-sources.jar" 2>/dev/null || true
|
||||||
|
|
||||||
|
cat > "${DST_DIR}/api-3.0.0-SNAPSHOT.pom" <<POM
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>pw.binom.ai.embeddingtext</groupId>
|
||||||
|
<artifactId>api</artifactId>
|
||||||
|
<version>3.0.0-SNAPSHOT</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
</project>
|
||||||
|
POM
|
||||||
|
|
||||||
|
cat > "${DST_DIR}/maven-metadata-local.xml" <<META
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<metadata>
|
||||||
|
<groupId>pw.binom.ai.embeddingtext</groupId>
|
||||||
|
<artifactId>api</artifactId>
|
||||||
|
<version>3.0.0-SNAPSHOT</version>
|
||||||
|
<versioning>
|
||||||
|
<snapshot><timestamp>20260101.000000</timestamp></snapshot>
|
||||||
|
<lastUpdated>20260101000000</lastUpdated>
|
||||||
|
</versioning>
|
||||||
|
</metadata>
|
||||||
|
META
|
||||||
|
|
||||||
|
echo "Installed stub: pw.binom.ai.embeddingtext:api:3.0.0-SNAPSHOT -> ${DST_DIR}"
|
||||||
@@ -12,6 +12,8 @@ dependencyResolutionManagement {
|
|||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
google()
|
google()
|
||||||
|
// Локально опубликованные snapshot-ы text-embedding-kmp (см. `~/.m2`).
|
||||||
|
mavenLocal()
|
||||||
// Home Nexus, репо "caffeine": pw.binom.* (A2A, ...)
|
// Home Nexus, репо "caffeine": pw.binom.* (A2A, ...)
|
||||||
maven {
|
maven {
|
||||||
name = "caffeine"
|
name = "caffeine"
|
||||||
|
|||||||
+33
-8
@@ -41,8 +41,11 @@ java -jar standalone/build/libs/standalone-all.jar
|
|||||||
| `AGENTIK_SKILLS_DIR` | _выкл._ | Каталог со скилами (`SKILL.md` / `*.yaml`) |
|
| `AGENTIK_SKILLS_DIR` | _выкл._ | Каталог со скилами (`SKILL.md` / `*.yaml`) |
|
||||||
| `AGENTIK_MEMORY_DIR` | `~/.agentik/memory` (md) или `AGENTIK_DB_PATH` (vector) | Каталог памяти (md); `"off"` отключает |
|
| `AGENTIK_MEMORY_DIR` | `~/.agentik/memory` (md) или `AGENTIK_DB_PATH` (vector) | Каталог памяти (md); `"off"` отключает |
|
||||||
| `AGENTIK_MEMORY_BACKEND` | `md` | `md` (Hermes-style §-файлы) / `vector` (SQLite + JVector + LLM-эмбеддинги) / `off` |
|
| `AGENTIK_MEMORY_BACKEND` | `md` | `md` (Hermes-style §-файлы) / `vector` (SQLite + JVector + LLM-эмбеддинги) / `off` |
|
||||||
| `AGENTIK_EMBEDDING_MODEL` | `text-embedding-3-small` | Модель эмбеддингов для vector-бэкенда |
|
| `AGENTIK_EMBEDDING_MODEL` | `text-embedding-3-small` | Модель эмбеддингов для vector-бэкенда (только HTTP) |
|
||||||
| `AGENTIK_EMBEDDING_DIMENSION` | `1536` | Размерность вектора (должна совпадать с моделью) |
|
| `AGENTIK_EMBEDDING_DIMENSION` | `1536` | Размерность вектора (только HTTP; SIGLIP определяет автоматически) |
|
||||||
|
| `AGENTIK_EMBEDDING_BACKEND` | `HTTP` | `HTTP` (POST /v1/embeddings) или `SIGLIP` (on-device, без сети) |
|
||||||
|
| `AGENTIK_EMBEDDING_MODEL_PATH` | _только SIGLIP_ | Путь к `text_model_int8.onnx` (SigLIP2) |
|
||||||
|
| `AGENTIK_EMBEDDING_TOKENIZER_PATH` | _только SIGLIP_ | Путь к `tokenizer.model` (sentencepiece) |
|
||||||
| `AGENTIK_SOUL` | _выкл._ | Путь к `SOUL.md` — файл персоны (markdown), вставляется в начало system prompt |
|
| `AGENTIK_SOUL` | _выкл._ | Путь к `SOUL.md` — файл персоны (markdown), вставляется в начало system prompt |
|
||||||
| `AGENTIK_LLM_BACKEND` | — | `openai` или `google` (см. ниже) |
|
| `AGENTIK_LLM_BACKEND` | — | `openai` или `google` (см. ниже) |
|
||||||
| `AGENTIK_MCP_CONFIG` | _выкл._ | Путь к JSON со списком MCP-серверов |
|
| `AGENTIK_MCP_CONFIG` | _выкл._ | Путь к JSON со списком MCP-серверов |
|
||||||
@@ -218,23 +221,45 @@ context…]` блок в начало user-сообщения; после зап
|
|||||||
`AGENTIK_MEMORY_BACKEND` выбирает хранилище. Дефолт — `md` (Hermes-style
|
`AGENTIK_MEMORY_BACKEND` выбирает хранилище. Дефолт — `md` (Hermes-style
|
||||||
§-файлы, keyword overlap, без внешних вызовов).
|
§-файлы, keyword overlap, без внешних вызовов).
|
||||||
|
|
||||||
`vector` — SQLite (`AGENTIK_DB_PATH`) + JVector ANN + LLM-эмбеддинги
|
`vector` — SQLite (`AGENTIK_DB_PATH`) + JVector ANN + эмбеддинги. Два
|
||||||
(запросы на `${OPENAI_BASE_URL}/v1/embeddings`). Семантический поиск:
|
бэкенда эмбеддингов через `AGENTIK_EMBEDDING_BACKEND`:
|
||||||
cosine similarity + recency-re-rank. Те же четыре тула; та же семантика.
|
|
||||||
|
- **`HTTP` (default)** — POST на `${OPENAI_BASE_URL}/v1/embeddings`. Семантический
|
||||||
|
поиск: cosine similarity + recency-re-rank.
|
||||||
|
- **`SIGLIP`** — on-device SigLIP2 через ONNX Runtime (text-embedding-kmp,
|
||||||
|
768-мерный вектор). Никаких внешних вызовов: модель и токенизатор должны
|
||||||
|
лежать на диске. Размерность определяется автоматически (768).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Vector-бэкенд
|
# Vector-бэкенд + HTTP-эмбеддинги (default)
|
||||||
AGENTIK_MEMORY_BACKEND=vector \
|
AGENTIK_MEMORY_BACKEND=vector \
|
||||||
|
AGENTIK_EMBEDDING_BACKEND=http \
|
||||||
AGENTIK_EMBEDDING_MODEL=text-embedding-3-small \
|
AGENTIK_EMBEDDING_MODEL=text-embedding-3-small \
|
||||||
AGENTIK_EMBEDDING_DIMENSION=1536 \
|
AGENTIK_EMBEDDING_DIMENSION=1536 \
|
||||||
java -jar standalone-all.jar
|
java -jar standalone-all.jar
|
||||||
|
|
||||||
|
# Vector-бэкенд + on-device SigLIP2 (без сети)
|
||||||
|
AGENTIK_MEMORY_BACKEND=vector \
|
||||||
|
AGENTIK_EMBEDDING_BACKEND=siglip \
|
||||||
|
AGENTIK_EMBEDDING_MODEL_PATH=/path/to/text_model_int8.onnx \
|
||||||
|
AGENTIK_EMBEDDING_TOKENIZER_PATH=/path/to/tokenizer.model \
|
||||||
|
java -jar standalone-all.jar
|
||||||
```
|
```
|
||||||
|
|
||||||
Для vector требуется `AGENTIK_LLM_BACKEND=openai` (т.к. нужен
|
Для HTTP-эмбеддингов требуется `AGENTIK_LLM_BACKEND=openai` (т.к. нужен
|
||||||
OpenAI-совместимый `/v1/embeddings` endpoint — LiteLLM proxy тоже подходит).
|
OpenAI-совместимый `/v1/embeddings` endpoint — LiteLLM proxy тоже подходит).
|
||||||
Embedding-вызовы кэшируются LRU на 256 текстов — дедупликация при повторных
|
HTTP-вызовы кэшируются LRU на 256 текстов — дедупликация при повторных
|
||||||
запросах одинаковых промптов.
|
запросах одинаковых промптов.
|
||||||
|
|
||||||
|
Для SIGLIP нужно сначала скачать модель (~283M) и токенизатор (~4M):
|
||||||
|
```bash
|
||||||
|
mkdir -p /path/to/siglip-model
|
||||||
|
curl -fSL -o /path/to/siglip-model/text_model_int8.onnx \
|
||||||
|
http://static.binom.pw/models/siglip2/text_model_int8.onnx
|
||||||
|
curl -fSL -o /path/to/siglip-model/tokenizer.model \
|
||||||
|
http://static.binom.pw/models/siglip2/tokenizer.model
|
||||||
|
```
|
||||||
|
|
||||||
> **Опционально:** при старте JVector может предупредить
|
> **Опционально:** при старте JVector может предупредить
|
||||||
> `Java vector incubator module is not readable`. Это значит, что JIT
|
> `Java vector incubator module is not readable`. Это значит, что JIT
|
||||||
> не использует SIMD (Panama Vector API) и индекс строится через скалярный
|
> не использует SIMD (Panama Vector API) и индекс строится через скалярный
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import pw.binom.agentik.memory.MemorySystem
|
|||||||
import pw.binom.agentik.memory.md.openMdMemorySystem
|
import pw.binom.agentik.memory.md.openMdMemorySystem
|
||||||
import pw.binom.agentik.memory.vector.VectorMemorySystem
|
import pw.binom.agentik.memory.vector.VectorMemorySystem
|
||||||
import pw.binom.agentik.memory.vector.embedding.HttpEmbeddingClient
|
import pw.binom.agentik.memory.vector.embedding.HttpEmbeddingClient
|
||||||
|
import pw.binom.agentik.memory.vector.embedding.SiglipEmbeddingProvider
|
||||||
import pw.binom.agentik.server.agentikAgent
|
import pw.binom.agentik.server.agentikAgent
|
||||||
import pw.binom.agentik.skills.SkillCatalog
|
import pw.binom.agentik.skills.SkillCatalog
|
||||||
import pw.binom.agentik.skills.SkillLoader
|
import pw.binom.agentik.skills.SkillLoader
|
||||||
@@ -93,25 +94,44 @@ fun main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
MemoryBackend.VECTOR -> {
|
MemoryBackend.VECTOR -> {
|
||||||
val llm = config.llm
|
val embedding: pw.binom.agentik.memory.vector.EmbeddingProvider = when (config.embeddingBackend) {
|
||||||
// Берём базовый URL + API key у активного LLM-бэкенда.
|
AgentikConfig.EmbeddingBackend.HTTP -> {
|
||||||
// Поддерживается только OPENAI (LiteLLM proxy тоже работает, т.к. /v1/embeddings
|
val llm = config.llm
|
||||||
// — это OpenAI-совместимый endpoint).
|
// Берём базовый URL + API key у активного LLM-бэкенда.
|
||||||
require(llm.backend == LlmBackend.OPENAI) {
|
// Поддерживается только OPENAI (LiteLLM proxy тоже работает, т.к. /v1/embeddings
|
||||||
"AGENTIK_MEMORY_BACKEND=vector требует LLM_BACKEND=openai (нужен /v1/embeddings)"
|
// — это OpenAI-совместимый endpoint).
|
||||||
|
require(llm.backend == LlmBackend.OPENAI) {
|
||||||
|
"AGENTIK_EMBEDDING_BACKEND=http требует LLM_BACKEND=openai (нужен /v1/embeddings)"
|
||||||
|
}
|
||||||
|
val oa = checkNotNull(llm.openai) { "openai config required for http embedding backend" }
|
||||||
|
HttpEmbeddingClient(
|
||||||
|
apiUrl = oa.baseUrl.trimEnd('/'),
|
||||||
|
apiKey = oa.apiKey,
|
||||||
|
model = config.embeddingModel,
|
||||||
|
dimension = config.embeddingDimension,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AgentikConfig.EmbeddingBackend.SIGLIP -> {
|
||||||
|
val modelPath = checkNotNull(config.embeddingModelPath) {
|
||||||
|
"AGENTIK_EMBEDDING_BACKEND=siglip требует AGENTIK_EMBEDDING_MODEL_PATH"
|
||||||
|
}
|
||||||
|
val tokenizerPath = checkNotNull(config.embeddingTokenizerPath) {
|
||||||
|
"AGENTIK_EMBEDDING_BACKEND=siglip требует AGENTIK_EMBEDDING_TOKENIZER_PATH"
|
||||||
|
}
|
||||||
|
SiglipEmbeddingProvider(modelPath = modelPath, tokenizerPath = tokenizerPath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val oa = checkNotNull(llm.openai) { "openai config required for vector backend" }
|
|
||||||
val embedding = HttpEmbeddingClient(
|
|
||||||
apiUrl = oa.baseUrl.trimEnd('/'),
|
|
||||||
apiKey = oa.apiKey,
|
|
||||||
model = config.embeddingModel,
|
|
||||||
dimension = config.embeddingDimension,
|
|
||||||
)
|
|
||||||
VectorMemorySystem.open(
|
VectorMemorySystem.open(
|
||||||
dbPath = config.dbPath,
|
dbPath = config.dbPath,
|
||||||
embedding = embedding,
|
embedding = embedding,
|
||||||
).also {
|
).also {
|
||||||
println(" memory: db=${config.dbPath} (vector-backend, model=${config.embeddingModel}, dim=${config.embeddingDimension})")
|
val backendLabel = when (config.embeddingBackend) {
|
||||||
|
AgentikConfig.EmbeddingBackend.HTTP ->
|
||||||
|
"model=${config.embeddingModel}, dim=${config.embeddingDimension}"
|
||||||
|
AgentikConfig.EmbeddingBackend.SIGLIP ->
|
||||||
|
"model=siglip2-base (on-device), dim=${embedding.dimension}"
|
||||||
|
}
|
||||||
|
println(" memory: db=${config.dbPath} (vector-backend, $backendLabel)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,20 +56,44 @@ data class AgentikConfig(
|
|||||||
*/
|
*/
|
||||||
val memoryBackend: MemoryBackend = MemoryBackend.MD,
|
val memoryBackend: MemoryBackend = MemoryBackend.MD,
|
||||||
/**
|
/**
|
||||||
* Имя модели эмбеддингов для vector-бэкенда. Дефолт `text-embedding-3-small`
|
* Имя модели эмбеддингов для vector-бэкенда. Используется только при
|
||||||
|
* [embeddingBackend] = HTTP. Дефолт `text-embedding-3-small`
|
||||||
* (1536-мерный). Должна быть доступна через тот же baseUrl/apiKey что и LLM.
|
* (1536-мерный). Должна быть доступна через тот же baseUrl/apiKey что и LLM.
|
||||||
*/
|
*/
|
||||||
val embeddingModel: String = DEFAULT_EMBEDDING_MODEL,
|
val embeddingModel: String = DEFAULT_EMBEDDING_MODEL,
|
||||||
/**
|
/**
|
||||||
* Размерность эмбеддингов vector-бэкенда. Должна совпадать с реальной
|
* Размерность эмбеддингов vector-бэкенда. Используется только при
|
||||||
* размерностью [embeddingModel]. Дефолт 1536 для `text-embedding-3-small`.
|
* [embeddingBackend] = HTTP. Должна совпадать с реальной размерностью
|
||||||
|
* [embeddingModel]. Дефолт 1536 для `text-embedding-3-small`.
|
||||||
|
* Для [embeddingBackend] = SIGLIP размерность определяется самой моделью
|
||||||
|
* (768 для SigLIP2-base), параметр игнорируется.
|
||||||
*/
|
*/
|
||||||
val embeddingDimension: Int = DEFAULT_EMBEDDING_DIMENSION,
|
val embeddingDimension: Int = DEFAULT_EMBEDDING_DIMENSION,
|
||||||
|
/**
|
||||||
|
* Бэкенд эмбеддингов для vector-памяти:
|
||||||
|
* - HTTP — POST /v1/embeddings к OpenAI-совместимому API (default);
|
||||||
|
* - SIGLIP — on-device SigLIP2 через ONNX Runtime (text-embedding-kmp), без сети.
|
||||||
|
*/
|
||||||
|
val embeddingBackend: EmbeddingBackend = EmbeddingBackend.HTTP,
|
||||||
|
/**
|
||||||
|
* Путь к ONNX-модели SigLIP2 (`text_model_int8.onnx`). Используется только при
|
||||||
|
* [embeddingBackend] = SIGLIP.
|
||||||
|
*/
|
||||||
|
val embeddingModelPath: String? = null,
|
||||||
|
/**
|
||||||
|
* Путь к sentencepiece-токенизатору (`tokenizer.model`). Используется только при
|
||||||
|
* [embeddingBackend] = SIGLIP.
|
||||||
|
*/
|
||||||
|
val embeddingTokenizerPath: String? = null,
|
||||||
) {
|
) {
|
||||||
/** Бэкенд долговременной памяти. */
|
/** Бэкенд долговременной памяти. */
|
||||||
@Serializable
|
@Serializable
|
||||||
enum class MemoryBackend { MD, VECTOR, OFF }
|
enum class MemoryBackend { MD, VECTOR, OFF }
|
||||||
|
|
||||||
|
/** Бэкенд эмбеддингов (для memory-backend=vector). */
|
||||||
|
@Serializable
|
||||||
|
enum class EmbeddingBackend { HTTP, SIGLIP }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val DEFAULT_PORT: Int = 8080
|
const val DEFAULT_PORT: Int = 8080
|
||||||
const val DEFAULT_DB_PATH: String = "./agentik.db"
|
const val DEFAULT_DB_PATH: String = "./agentik.db"
|
||||||
@@ -99,6 +123,11 @@ data class AgentikConfig(
|
|||||||
?: DEFAULT_EMBEDDING_MODEL,
|
?: DEFAULT_EMBEDDING_MODEL,
|
||||||
embeddingDimension = env("AGENTIK_EMBEDDING_DIMENSION")?.toIntOrNull()
|
embeddingDimension = env("AGENTIK_EMBEDDING_DIMENSION")?.toIntOrNull()
|
||||||
?: DEFAULT_EMBEDDING_DIMENSION,
|
?: DEFAULT_EMBEDDING_DIMENSION,
|
||||||
|
embeddingBackend = env("AGENTIK_EMBEDDING_BACKEND")?.let {
|
||||||
|
runCatching { EmbeddingBackend.valueOf(it.uppercase()) }.getOrNull()
|
||||||
|
} ?: EmbeddingBackend.HTTP,
|
||||||
|
embeddingModelPath = env("AGENTIK_EMBEDDING_MODEL_PATH")?.takeIf { it.isNotBlank() },
|
||||||
|
embeddingTokenizerPath = env("AGENTIK_EMBEDDING_TOKENIZER_PATH")?.takeIf { it.isNotBlank() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user