Upgrade litert-api/litert-google/litert-openai to v7 (litertlm-jvm 0.17.0), drop workarounds
This commit is contained in:
+5
-9
@@ -1,6 +1,6 @@
|
|||||||
# Standalone — рантайм агента agentik
|
# Standalone — рантайм агента agentik
|
||||||
|
|
||||||
`standalone` — это исполняемое JVM-приложение (точка входа `pw.binom.agentik.standalone.MainKt`), которое поднимает реальный агент `ChatAgent` (stateful, SQLite-персистентный) и навешивает на него HTTP+SSE фасад `:server`. LLM-движок выбирается через `AGENTIK_LLM_BACKEND` — на v1 поддерживаются `litert-openai` (любой OpenAI-совместимый endpoint) и `litert-google` (on-device движок LiteRT-LM 0.16.1 через нативную `.so`-библиотеку).
|
`standalone` — это исполняемое JVM-приложение (точка входа `pw.binom.agentik.standalone.MainKt`), которое поднимает реальный агент `ChatAgent` (stateful, SQLite-персистентный) и навешивает на него HTTP+SSE фасад `:server`. LLM-движок выбирается через `AGENTIK_LLM_BACKEND` — на v1 поддерживаются `litert-openai` (любой OpenAI-совместимый endpoint) и `litert-google` (on-device движок LiteRT-LM 0.17.0 через нативную `.so`-библиотеку из litertlm-jvm 0.17.0).
|
||||||
|
|
||||||
Этот документ описывает, как `standalone` собран и как его расширять.
|
Этот документ описывает, как `standalone` собран и как его расширять.
|
||||||
|
|
||||||
@@ -209,7 +209,7 @@ suspend fun touch(id: String, now: Instant)
|
|||||||
`LlmConfig.fromEnv()` парсит env, валидирует обязательные поля и выбирает бэкенд через `AGENTIK_LLM_BACKEND`:
|
`LlmConfig.fromEnv()` парсит env, валидирует обязательные поля и выбирает бэкенд через `AGENTIK_LLM_BACKEND`:
|
||||||
|
|
||||||
- `openai` (default) — `litert-openai`, текст-онли чат против любого OpenAI-совместимого endpoint.
|
- `openai` (default) — `litert-openai`, текст-онли чат против любого OpenAI-совместимого endpoint.
|
||||||
- `google` — `litert-google` (LiteRT-LM 0.16.1, on-device `.task`/`.litertlm` модель, требует нативной библиотеки через `litertlm-jvm`).
|
- `google` — `litert-google` (LiteRT-LM 0.17.0, on-device `.task`/`.litertlm` модель, требует нативной библиотеки через `litertlm-jvm`).
|
||||||
|
|
||||||
### Общие env
|
### Общие env
|
||||||
|
|
||||||
@@ -238,15 +238,11 @@ suspend fun touch(id: String, now: Instant)
|
|||||||
|
|
||||||
`AGENTIK_DB_PATH=:memory:` создаёт in-memory БД (только для тестов и интеграционных проверок).
|
`AGENTIK_DB_PATH=:memory:` создаёт in-memory БД (только для тестов и интеграционных проверок).
|
||||||
|
|
||||||
### Long-lived LiteConversation — ОБЯЗАТЕЛЬНО для google
|
### Long-lived LiteConversation — ОБЯЗАТЕЛЬНО для обоих бэкендов
|
||||||
|
|
||||||
`LiteConversation` от любого litert-бэкенда — это **долгоживущая stateful ручка**: она держит историю сообщений и (для google) KV-cache/sampler-state. На v1 `ChatConversation` создаёт `LiteConversation` один раз (на первом `send`) и переиспользует на всех последующих turn'ах той же беседы. Пересоздание LiteConversation на каждый send ломает KV-cache для google (и в v1 приводит к ошибке chat template «roles must alternate» после двух user-сообщений).
|
`LiteConversation` от любого litert-бэкенда — это **долгоживущая stateful ручка**: она держит историю сообщений и (для google) KV-cache/sampler-state. `ChatConversation` создаёт `LiteConversation` один раз (на первом `send`) и переиспользует на всех последующих turn'ах той же беседы. Пересоздание LiteConversation на каждый send ломает KV-cache для google (LiteRT-LM 0.17.0 умеет правильно восстанавливать state при `systemInstruction + пары User/Assistant` в `initialMessages`).
|
||||||
|
|
||||||
`AGENTIK_GOOGLE_FOLD_SYSTEM_INTO_FIRST_USER` (default `true` для google) — фолдит системный промпт в первое user-сообщение, чтобы движок увидел только `[User+system, Assistant, User, ...]` и не падал на alternation. Для openai этот режим отключён.
|
`LiteLlm.capabilities: LiteCapabilities?` (litert-api 7+) — `litert-google` читает из заголовка on-disk модели (text/vision/audio, supportsThinking, supportsFunctionCalling, maxVisionTokenBudget); `litert-openai` возвращает `null` (модель не хранится на диске). Используй для фильтрации модальностей в клиенте.
|
||||||
|
|
||||||
### Workaround для litert-google-jvm 0.16.1
|
|
||||||
|
|
||||||
Баг в `sendStreamContents` — поток не закрывается после `isDone = true`. `ChatConversation` оборачивает стрим в `transformWhile { !delta.isDone }`, поэтому SSE получает корректный `End` event.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ kotlinx-datetime = "0.8.0"
|
|||||||
ktor = "3.1.3"
|
ktor = "3.1.3"
|
||||||
agui = "0.1.0"
|
agui = "0.1.0"
|
||||||
a2a = "1.0.0-SNAPSHOT"
|
a2a = "1.0.0-SNAPSHOT"
|
||||||
litert = "6"
|
litert = "7"
|
||||||
sqldelight = "2.3.2"
|
sqldelight = "2.3.2"
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class ChatAgent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val conv = ChatConversation(record = rec, stores = stores, llm = llm, systemPrompt = llmConfig.systemPrompt, foldSystemIntoFirstUser = llmConfig.foldSystemIntoFirstUser)
|
val conv = ChatConversation(record = rec, stores = stores, llm = llm, systemPrompt = llmConfig.systemPrompt)
|
||||||
runBlocking {
|
runBlocking {
|
||||||
liveLock.withLock { live[conv.id] = conv }
|
liveLock.withLock { live[conv.id] = conv }
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,7 @@ class ChatAgent(
|
|||||||
override suspend fun getConversation(id: String): ProtoConversation? {
|
override suspend fun getConversation(id: String): ProtoConversation? {
|
||||||
liveLock.withLock { live[id] }?.let { if (!it.isClosed) return it }
|
liveLock.withLock { live[id] }?.let { if (!it.isClosed) return it }
|
||||||
val rec = stores.conversations.get(id) ?: return null
|
val rec = stores.conversations.get(id) ?: return null
|
||||||
return ChatConversation(record = rec, stores = stores, llm = llm, systemPrompt = llmConfig.systemPrompt, foldSystemIntoFirstUser = llmConfig.foldSystemIntoFirstUser).also {
|
return ChatConversation(record = rec, stores = stores, llm = llm, systemPrompt = llmConfig.systemPrompt).also {
|
||||||
liveLock.withLock { live[id] = it }
|
liveLock.withLock { live[id] = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-26
@@ -9,7 +9,6 @@ import kotlinx.coroutines.channels.BufferOverflow
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.transformWhile
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
@@ -59,7 +58,6 @@ class ChatConversation(
|
|||||||
private val stores: SqliteStores,
|
private val stores: SqliteStores,
|
||||||
private val llm: LiteLlm,
|
private val llm: LiteLlm,
|
||||||
private val systemPrompt: String,
|
private val systemPrompt: String,
|
||||||
private val foldSystemIntoFirstUser: Boolean = false,
|
|
||||||
) : ProtoConversation, AutoCloseable {
|
) : ProtoConversation, AutoCloseable {
|
||||||
|
|
||||||
private var record: ConversationRecord = record
|
private var record: ConversationRecord = record
|
||||||
@@ -184,12 +182,7 @@ class ChatConversation(
|
|||||||
|
|
||||||
val reply = StringBuilder()
|
val reply = StringBuilder()
|
||||||
try {
|
try {
|
||||||
// Некоторые бэкенды (litert-google-jvm 0.16.1) не закрывают стрим после `isDone = true`,
|
liteConv.sendStreamContents(parts).collect { delta ->
|
||||||
// поэтому заворачиваем в transformWhile: видим isDone → отдаём дельту и завершаем flow.
|
|
||||||
liteConv.sendStreamContents(parts).transformWhile { delta ->
|
|
||||||
emit(delta)
|
|
||||||
!delta.isDone
|
|
||||||
}.collect { delta ->
|
|
||||||
if (delta.text.isNotEmpty()) {
|
if (delta.text.isNotEmpty()) {
|
||||||
reply.append(delta.text)
|
reply.append(delta.text)
|
||||||
emitEvent(ProtoEvent.AppendText(date = now(), body = delta.text))
|
emitEvent(ProtoEvent.AppendText(date = now(), body = delta.text))
|
||||||
@@ -238,9 +231,6 @@ class ChatConversation(
|
|||||||
* уже записанное в audit + working memory, но ещё не отправленное в LLM — мы отдадим
|
* уже записанное в audit + working memory, но ещё не отправленное в LLM — мы отдадим
|
||||||
* его через [LiteConversation.sendStreamContents]). Это предотвращает дублирование
|
* его через [LiteConversation.sendStreamContents]). Это предотвращает дублирование
|
||||||
* "user → user" в LiteConversation history.
|
* "user → user" в LiteConversation history.
|
||||||
*
|
|
||||||
* Если [foldSystemIntoFirstUser] — система не передаётся как `systemInstruction`,
|
|
||||||
* а фолдится в первое user-сообщение (нужно для Gemma-3 шаблона LiteRT-LM 0.16.1).
|
|
||||||
*/
|
*/
|
||||||
private suspend fun getOrCreateLiteConversation(excludeUserSourceId: String? = null): LiteConversation {
|
private suspend fun getOrCreateLiteConversation(excludeUserSourceId: String? = null): LiteConversation {
|
||||||
liteConv?.let { return it }
|
liteConv?.let { return it }
|
||||||
@@ -268,21 +258,8 @@ class ChatConversation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val config = LiteConversationConfig(
|
val config = LiteConversationConfig(
|
||||||
systemInstruction = if (foldSystemIntoFirstUser) null else resolvedSystemPrompt.takeIf { it.isNotBlank() },
|
systemInstruction = resolvedSystemPrompt.takeIf { it.isNotBlank() },
|
||||||
initialMessages = if (foldSystemIntoFirstUser) {
|
initialMessages = pastTurns,
|
||||||
val firstUser = pastTurns.firstOrNull { it.role == LiteRole.USER }
|
|
||||||
if (firstUser != null && resolvedSystemPrompt.isNotBlank()) {
|
|
||||||
val folded = LiteMessage(
|
|
||||||
role = LiteRole.USER,
|
|
||||||
contents = listOf(LiteContentPart.Text(resolvedSystemPrompt + "\n\n")) + firstUser.contents,
|
|
||||||
)
|
|
||||||
listOf(folded) + pastTurns.drop(1)
|
|
||||||
} else {
|
|
||||||
pastTurns
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pastTurns
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return llm.createConversation(config).also { liteConv = it }
|
return llm.createConversation(config).also { liteConv = it }
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ data class LlmConfig(
|
|||||||
val systemPrompt: String,
|
val systemPrompt: String,
|
||||||
val openai: LitertOpenAiConfig? = null,
|
val openai: LitertOpenAiConfig? = null,
|
||||||
val google: GoogleConfig? = null,
|
val google: GoogleConfig? = null,
|
||||||
val foldSystemIntoFirstUser: Boolean = backend == LlmBackend.GOOGLE,
|
|
||||||
) {
|
) {
|
||||||
init {
|
init {
|
||||||
when (backend) {
|
when (backend) {
|
||||||
|
|||||||
@@ -326,6 +326,7 @@ class ChatAgentTest {
|
|||||||
|
|
||||||
private fun fakeLiteLlmForReload(): LiteLlm = object : LiteLlm {
|
private fun fakeLiteLlmForReload(): LiteLlm = object : LiteLlm {
|
||||||
override val backendName: String = "fake"
|
override val backendName: String = "fake"
|
||||||
|
override val capabilities: pw.binom.litert.LiteCapabilities? = null
|
||||||
override fun isInitialized(): Boolean = true
|
override fun isInitialized(): Boolean = true
|
||||||
override fun createConversation(config: LiteConversationConfig): LiteConversation =
|
override fun createConversation(config: LiteConversationConfig): LiteConversation =
|
||||||
error("not used in reload test")
|
error("not used in reload test")
|
||||||
@@ -357,6 +358,7 @@ class ChatAgentTest {
|
|||||||
/** Поддельный LiteLlm: возвращает fakeLlm.reply в sendStreamContents, опционально запоминает history. */
|
/** Поддельный LiteLlm: возвращает fakeLlm.reply в sendStreamContents, опционально запоминает history. */
|
||||||
private class FakeLiteLlm : LiteLlm {
|
private class FakeLiteLlm : LiteLlm {
|
||||||
override val backendName: String = "fake"
|
override val backendName: String = "fake"
|
||||||
|
override val capabilities: pw.binom.litert.LiteCapabilities? = null
|
||||||
var reply: String = ""
|
var reply: String = ""
|
||||||
var rememberHistory: Boolean = false
|
var rememberHistory: Boolean = false
|
||||||
var slow: Boolean = false
|
var slow: Boolean = false
|
||||||
|
|||||||
Reference in New Issue
Block a user