standalone: ChatAgent принимает StorageBundle вместо SqliteStores
Финальный swap — ChatAgent/ChatConversation теперь работают через абстрактный
StorageBundle (pw.binom.agentik.storage), а не через конкретный SqliteStores.
Подготовка к Android-портированию (там будет :storage-android вместо
:storage-sqlite).
Изменения:
- SqliteStores.asBundle() — convenience для превращения конкретного
SQLite-импла в StorageBundle
- ChatAgent(private val storage: StorageBundle) — было stores: SqliteStores
- ChatConversation(private val storage: StorageBundle) — то же
- Main.kt, DebugRoutes.kt — вызовы обновлены, используется .asBundle()
- Все 5 тестовых файлов с ChatAgent(... stores = ...) — обновлены на
ChatAgent(... storage = ...asBundle())
- StorageBundle : AutoCloseable — закрывает все 4 store'а; в тестах
tearDown { storage.close() }
Конфиг не менялся: toolsets остаётся emptyList() по умолчанию (полная
невидимость механики тулсетов для модели). Подключение тулсетов — opt-in
через параметр ChatAgent(toolsets = ...) для будущего e2e-теста в post-implementation.
Tests: 340/340 green. Fatjar 240 MB. Без регрессий.
This commit is contained in:
@@ -14,7 +14,7 @@ import pw.binom.agentik.standalone.agent.ChatConversation
|
|||||||
import pw.binom.agentik.standalone.agent.LlmReflector
|
import pw.binom.agentik.standalone.agent.LlmReflector
|
||||||
import pw.binom.agentik.standalone.agent.SkillMiner
|
import pw.binom.agentik.standalone.agent.SkillMiner
|
||||||
import pw.binom.agentik.standalone.agent.memory.Curator
|
import pw.binom.agentik.standalone.agent.memory.Curator
|
||||||
import pw.binom.agentik.storage.sqlite.SqliteStores
|
import pw.binom.agentik.storage.StorageBundle
|
||||||
import pw.binom.agentik.skills.SkillStore
|
import pw.binom.agentik.skills.SkillStore
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,7 +33,7 @@ import pw.binom.agentik.skills.SkillStore
|
|||||||
*/
|
*/
|
||||||
internal fun Route.debugRoutes(
|
internal fun Route.debugRoutes(
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
stores: SqliteStores,
|
storage: pw.binom.agentik.storage.StorageBundle,
|
||||||
reflector: LlmReflector?,
|
reflector: LlmReflector?,
|
||||||
skillMiner: SkillMiner?,
|
skillMiner: SkillMiner?,
|
||||||
skillStore: SkillStore?,
|
skillStore: SkillStore?,
|
||||||
@@ -44,14 +44,14 @@ internal fun Route.debugRoutes(
|
|||||||
?: return@post call.respondText("conversationId required", status = HttpStatusCode.BadRequest)
|
?: return@post call.respondText("conversationId required", status = HttpStatusCode.BadRequest)
|
||||||
val minerReflector = reflector
|
val minerReflector = reflector
|
||||||
if (minerReflector == null) return@post call.respondText("reflection disabled", status = HttpStatusCode.NotFound)
|
if (minerReflector == null) return@post call.respondText("reflection disabled", status = HttpStatusCode.NotFound)
|
||||||
val turns = recentTurns(stores, convId, maxTurns = 6)
|
val turns = recentTurns(storage, convId, maxTurns = 6)
|
||||||
if (turns.isEmpty()) return@post call.respondText("no turns in conversation", status = HttpStatusCode.NotFound)
|
if (turns.isEmpty()) return@post call.respondText("no turns in conversation", status = HttpStatusCode.NotFound)
|
||||||
val reflection = minerReflector.reflect(turns)
|
val reflection = minerReflector.reflect(turns)
|
||||||
if (reflection == null) {
|
if (reflection == null) {
|
||||||
call.respondText("""{"reflected":false,"reason":"unparseable LLM reply"}""", contentType = ContentType.Application.Json)
|
call.respondText("""{"reflected":false,"reason":"unparseable LLM reply"}""", contentType = ContentType.Application.Json)
|
||||||
} else {
|
} else {
|
||||||
val stamped = reflection.copy(conversationId = convId)
|
val stamped = reflection.copy(conversationId = convId)
|
||||||
stores.reflections.insert(stamped)
|
storage.reflectionStore.insert(stamped)
|
||||||
call.respondText(
|
call.respondText(
|
||||||
buildJsonObject {
|
buildJsonObject {
|
||||||
put("reflected", true)
|
put("reflected", true)
|
||||||
@@ -71,7 +71,7 @@ internal fun Route.debugRoutes(
|
|||||||
val miner = skillMiner
|
val miner = skillMiner
|
||||||
val store = skillStore
|
val store = skillStore
|
||||||
if (miner == null || store == null) return@post call.respondText("skill mining disabled", status = HttpStatusCode.NotFound)
|
if (miner == null || store == null) return@post call.respondText("skill mining disabled", status = HttpStatusCode.NotFound)
|
||||||
val turns = recentTurns(stores, convId, maxTurns = miner.maxTurns)
|
val turns = recentTurns(storage, convId, maxTurns = miner.maxTurns)
|
||||||
if (turns.isEmpty()) return@post call.respondText("no turns in conversation", status = HttpStatusCode.NotFound)
|
if (turns.isEmpty()) return@post call.respondText("no turns in conversation", status = HttpStatusCode.NotFound)
|
||||||
val mined = miner.mine(turns, store.catalog.skills)
|
val mined = miner.mine(turns, store.catalog.skills)
|
||||||
for (s in mined) store.upsert(s)
|
for (s in mined) store.upsert(s)
|
||||||
@@ -110,7 +110,7 @@ internal fun Route.debugRoutes(
|
|||||||
get("/debug/tokens") {
|
get("/debug/tokens") {
|
||||||
val convId = call.parameters["conversationId"]
|
val convId = call.parameters["conversationId"]
|
||||||
?: return@get call.respondText("conversationId required", status = HttpStatusCode.BadRequest)
|
?: return@get call.respondText("conversationId required", status = HttpStatusCode.BadRequest)
|
||||||
val stats = stores.messages.tokenStats(convId)
|
val stats = storage.messageStore.tokenStats(convId)
|
||||||
val json = buildJsonObject {
|
val json = buildJsonObject {
|
||||||
put("conversationId", convId)
|
put("conversationId", convId)
|
||||||
put("turns", stats.turns.toString())
|
put("turns", stats.turns.toString())
|
||||||
@@ -127,8 +127,8 @@ internal fun Route.debugRoutes(
|
|||||||
* (для debug-триггеров reflector/miner; та же логика, что у хуков
|
* (для debug-триггеров reflector/miner; та же логика, что у хуков
|
||||||
* [ChatConversation]).
|
* [ChatConversation]).
|
||||||
*/
|
*/
|
||||||
internal suspend fun recentTurns(stores: SqliteStores, conversationId: String, maxTurns: Int): List<ConversationTurn> {
|
internal suspend fun recentTurns(storage: pw.binom.agentik.storage.StorageBundle, conversationId: String, maxTurns: Int): List<ConversationTurn> {
|
||||||
val rows = stores.workingMemory.list(conversationId)
|
val rows = storage.workingMemoryStore.list(conversationId)
|
||||||
val pairs = mutableListOf<ConversationTurn>()
|
val pairs = mutableListOf<ConversationTurn>()
|
||||||
var pendingUser: String? = null
|
var pendingUser: String? = null
|
||||||
for (row in rows) {
|
for (row in rows) {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ fun main() {
|
|||||||
val config = AgentikConfig.fromEnv()
|
val config = AgentikConfig.fromEnv()
|
||||||
|
|
||||||
val llm = config.llm.createLlm()
|
val llm = config.llm.createLlm()
|
||||||
val stores = SqliteStores.open(dbPath = config.dbPath)
|
val storage = SqliteStores.open(dbPath = config.dbPath).asBundle()
|
||||||
val mcpRegistry = McpRegistry.fromConfig(config.mcp)
|
val mcpRegistry = McpRegistry.fromConfig(config.mcp)
|
||||||
|
|
||||||
// Хранилище скилов: если skillsDir задан, читаем каталог + создаём
|
// Хранилище скилов: если skillsDir задан, читаем каталог + создаём
|
||||||
@@ -179,12 +179,12 @@ fun main() {
|
|||||||
if (config.reflectionInterval > 0) LlmReflector(llm = llm) else null
|
if (config.reflectionInterval > 0) LlmReflector(llm = llm) else null
|
||||||
val recentReflections: List<pw.binom.agentik.storage.Reflection> =
|
val recentReflections: List<pw.binom.agentik.storage.Reflection> =
|
||||||
if (config.reflectionTopK > 0) kotlinx.coroutines.runBlocking {
|
if (config.reflectionTopK > 0) kotlinx.coroutines.runBlocking {
|
||||||
stores.reflections.listRecent(config.reflectionTopK)
|
storage.reflectionStore.listRecent(config.reflectionTopK)
|
||||||
} else emptyList()
|
} else emptyList()
|
||||||
|
|
||||||
val agent = ChatAgent(
|
val agent = ChatAgent(
|
||||||
id = "agentik",
|
id = "agentik",
|
||||||
stores = stores,
|
storage = storage,
|
||||||
llm = llm,
|
llm = llm,
|
||||||
llmConfig = config.llm,
|
llmConfig = config.llm,
|
||||||
tools = mcpRegistry.namedTools,
|
tools = mcpRegistry.namedTools,
|
||||||
@@ -222,7 +222,7 @@ fun main() {
|
|||||||
if (config.debugEndpoints) {
|
if (config.debugEndpoints) {
|
||||||
debugRoutes(
|
debugRoutes(
|
||||||
agent = agent,
|
agent = agent,
|
||||||
stores = stores,
|
storage = storage,
|
||||||
reflector = reflector,
|
reflector = reflector,
|
||||||
skillMiner = skillMiner,
|
skillMiner = skillMiner,
|
||||||
skillStore = skillStore,
|
skillStore = skillStore,
|
||||||
@@ -260,14 +260,14 @@ fun main() {
|
|||||||
// Token stats по существующим диалогам (агрегат на старте — каждая запись
|
// Token stats по существующим диалогам (агрегат на старте — каждая запись
|
||||||
// парсится из payload_json, ну >100 turns и БД приличная — но в рамках
|
// парсится из payload_json, ну >100 turns и БД приличная — но в рамках
|
||||||
// стартапа это терпимо).
|
// стартапа это терпимо).
|
||||||
val existingConvs = kotlinx.coroutines.runBlocking { stores.conversations.list(offset = 0, limit = 1000) }
|
val existingConvs = kotlinx.coroutines.runBlocking { storage.conversationStore.list(offset = 0, limit = 1000) }
|
||||||
if (existingConvs.isNotEmpty()) {
|
if (existingConvs.isNotEmpty()) {
|
||||||
var totalTurns = 0
|
var totalTurns = 0
|
||||||
var totalIn = 0L
|
var totalIn = 0L
|
||||||
var totalOut = 0L
|
var totalOut = 0L
|
||||||
for (c in existingConvs) {
|
for (c in existingConvs) {
|
||||||
if (c.isTemporal) continue
|
if (c.isTemporal) continue
|
||||||
val s = kotlinx.coroutines.runBlocking { stores.messages.tokenStats(c.id) }
|
val s = kotlinx.coroutines.runBlocking { storage.messageStore.tokenStats(c.id) }
|
||||||
totalTurns += s.turns
|
totalTurns += s.turns
|
||||||
totalIn += s.inputTokens
|
totalIn += s.inputTokens
|
||||||
totalOut += s.outputTokens
|
totalOut += s.outputTokens
|
||||||
@@ -279,7 +279,7 @@ fun main() {
|
|||||||
Runtime.getRuntime().addShutdownHook(Thread {
|
Runtime.getRuntime().addShutdownHook(Thread {
|
||||||
agent.close()
|
agent.close()
|
||||||
mcpRegistry.close()
|
mcpRegistry.close()
|
||||||
stores.close()
|
storage.close()
|
||||||
llm.close()
|
llm.close()
|
||||||
memorySystem?.close()
|
memorySystem?.close()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import pw.binom.agentik.standalone.llm.LlmConfig
|
|||||||
import pw.binom.agentik.storage.ConversationRecord
|
import pw.binom.agentik.storage.ConversationRecord
|
||||||
import pw.binom.agentik.storage.Reflection
|
import pw.binom.agentik.storage.Reflection
|
||||||
import pw.binom.agentik.storage.WorkingMemoryEntry
|
import pw.binom.agentik.storage.WorkingMemoryEntry
|
||||||
import pw.binom.agentik.storage.sqlite.SqliteStores
|
import pw.binom.agentik.storage.StorageBundle
|
||||||
import pw.binom.agentik.toolsets.EnableToolsetTool
|
import pw.binom.agentik.toolsets.EnableToolsetTool
|
||||||
import pw.binom.agentik.toolsets.DisableToolsetTool
|
import pw.binom.agentik.toolsets.DisableToolsetTool
|
||||||
import pw.binom.agentik.toolsets.SystemPromptToolsetSection
|
import pw.binom.agentik.toolsets.SystemPromptToolsetSection
|
||||||
@@ -51,7 +51,7 @@ import kotlin.time.Instant
|
|||||||
*/
|
*/
|
||||||
class ChatAgent(
|
class ChatAgent(
|
||||||
override val id: String,
|
override val id: String,
|
||||||
private val stores: SqliteStores,
|
private val storage: pw.binom.agentik.storage.StorageBundle,
|
||||||
private val llm: LiteLlm,
|
private val llm: LiteLlm,
|
||||||
private val llmConfig: LlmConfig,
|
private val llmConfig: LlmConfig,
|
||||||
private val tools: List<NamedTool> = emptyList(),
|
private val tools: List<NamedTool> = emptyList(),
|
||||||
@@ -211,8 +211,8 @@ class ChatAgent(
|
|||||||
// и не переживают рестарт агента (см. Memory #3709).
|
// и не переживают рестарт агента (см. Memory #3709).
|
||||||
if (!temp) {
|
if (!temp) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
stores.conversations.upsert(rec)
|
storage.conversationStore.upsert(rec)
|
||||||
stores.workingMemory.append(
|
storage.workingMemoryStore.append(
|
||||||
conversationId = id,
|
conversationId = id,
|
||||||
entry = WorkingMemoryEntry.System(text = systemPrompt),
|
entry = WorkingMemoryEntry.System(text = systemPrompt),
|
||||||
now = now,
|
now = now,
|
||||||
@@ -221,7 +221,7 @@ class ChatAgent(
|
|||||||
}
|
}
|
||||||
val conv = ChatConversation(
|
val conv = ChatConversation(
|
||||||
record = rec,
|
record = rec,
|
||||||
stores = stores,
|
storage = storage,
|
||||||
llm = llm,
|
llm = llm,
|
||||||
systemPrompt = systemPrompt,
|
systemPrompt = systemPrompt,
|
||||||
tools = allTools,
|
tools = allTools,
|
||||||
@@ -232,7 +232,7 @@ class ChatAgent(
|
|||||||
contextWindow = contextWindow,
|
contextWindow = contextWindow,
|
||||||
compressionThreshold = compressionThreshold,
|
compressionThreshold = compressionThreshold,
|
||||||
contextCompactor = contextCompactor,
|
contextCompactor = contextCompactor,
|
||||||
reflectionStore = stores.reflections,
|
reflectionStore = storage.reflectionStore,
|
||||||
reflector = reflector,
|
reflector = reflector,
|
||||||
reflectionInterval = reflectionInterval,
|
reflectionInterval = reflectionInterval,
|
||||||
skillMiner = skillMiner,
|
skillMiner = skillMiner,
|
||||||
@@ -248,7 +248,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 = storage.conversationStore.get(id) ?: return null
|
||||||
return newConversation(rec).also {
|
return newConversation(rec).also {
|
||||||
liveLock.withLock { live[id] = it }
|
liveLock.withLock { live[id] = it }
|
||||||
}
|
}
|
||||||
@@ -257,13 +257,13 @@ class ChatAgent(
|
|||||||
override suspend fun deleteConversation(id: String): Boolean {
|
override suspend fun deleteConversation(id: String): Boolean {
|
||||||
val conv = liveLock.withLock { live.remove(id) }
|
val conv = liveLock.withLock { live.remove(id) }
|
||||||
conv?.close()
|
conv?.close()
|
||||||
val ok = stores.conversations.delete(id)
|
val ok = storage.conversationStore.delete(id)
|
||||||
if (ok) agentEvents.tryEmit(AgentEvent.Deleted(date = now(), id = id))
|
if (ok) agentEvents.tryEmit(AgentEvent.Deleted(date = now(), id = id))
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getConversations(offset: Int, limit: Int): List<ProtoConversation> =
|
override suspend fun getConversations(offset: Int, limit: Int): List<ProtoConversation> =
|
||||||
stores.conversations.list(offset = offset, limit = limit).map { rec ->
|
storage.conversationStore.list(offset = offset, limit = limit).map { rec ->
|
||||||
liveLock.withLock { live[rec.id] }
|
liveLock.withLock { live[rec.id] }
|
||||||
?: newConversation(rec).also {
|
?: newConversation(rec).also {
|
||||||
liveLock.withLock { live[rec.id] = it }
|
liveLock.withLock { live[rec.id] = it }
|
||||||
@@ -272,7 +272,7 @@ class ChatAgent(
|
|||||||
|
|
||||||
private fun newConversation(rec: ConversationRecord): ChatConversation = ChatConversation(
|
private fun newConversation(rec: ConversationRecord): ChatConversation = ChatConversation(
|
||||||
record = rec,
|
record = rec,
|
||||||
stores = stores,
|
storage = storage,
|
||||||
llm = llm,
|
llm = llm,
|
||||||
systemPrompt = systemPrompt,
|
systemPrompt = systemPrompt,
|
||||||
tools = allTools,
|
tools = allTools,
|
||||||
@@ -283,7 +283,7 @@ class ChatAgent(
|
|||||||
contextWindow = contextWindow,
|
contextWindow = contextWindow,
|
||||||
compressionThreshold = compressionThreshold,
|
compressionThreshold = compressionThreshold,
|
||||||
contextCompactor = contextCompactor,
|
contextCompactor = contextCompactor,
|
||||||
reflectionStore = stores.reflections,
|
reflectionStore = storage.reflectionStore,
|
||||||
reflector = reflector,
|
reflector = reflector,
|
||||||
reflectionInterval = reflectionInterval,
|
reflectionInterval = reflectionInterval,
|
||||||
skillMiner = skillMiner,
|
skillMiner = skillMiner,
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import pw.binom.agentik.storage.MessageStore
|
|||||||
import pw.binom.agentik.storage.WorkingMemoryEntry
|
import pw.binom.agentik.storage.WorkingMemoryEntry
|
||||||
import pw.binom.agentik.storage.WorkingMemoryRow
|
import pw.binom.agentik.storage.WorkingMemoryRow
|
||||||
import pw.binom.agentik.storage.WorkingMemoryStore
|
import pw.binom.agentik.storage.WorkingMemoryStore
|
||||||
import pw.binom.agentik.storage.sqlite.SqliteStores
|
|
||||||
import pw.binom.agentik.toolsets.ToolsetDispatchPolicy
|
import pw.binom.agentik.toolsets.ToolsetDispatchPolicy
|
||||||
import pw.binom.litert.LiteContentPart
|
import pw.binom.litert.LiteContentPart
|
||||||
import pw.binom.litert.LiteConversation
|
import pw.binom.litert.LiteConversation
|
||||||
@@ -71,7 +70,7 @@ import kotlin.time.Instant
|
|||||||
*/
|
*/
|
||||||
class ChatConversation(
|
class ChatConversation(
|
||||||
record: ConversationRecord,
|
record: ConversationRecord,
|
||||||
private val stores: SqliteStores,
|
private val storage: pw.binom.agentik.storage.StorageBundle,
|
||||||
private val llm: LiteLlm,
|
private val llm: LiteLlm,
|
||||||
private val systemPrompt: String,
|
private val systemPrompt: String,
|
||||||
private val tools: List<NamedTool> = emptyList(),
|
private val tools: List<NamedTool> = emptyList(),
|
||||||
@@ -152,9 +151,9 @@ class ChatConversation(
|
|||||||
override val title: String? get() = record.title
|
override val title: String? get() = record.title
|
||||||
override val updatedAt: Instant get() = record.updatedAt
|
override val updatedAt: Instant get() = record.updatedAt
|
||||||
|
|
||||||
private val conversationStore: ConversationStore get() = stores.conversations
|
private val conversationStore: ConversationStore get() = storage.conversationStore
|
||||||
private val messageStore: MessageStore get() = stores.messages
|
private val messageStore: MessageStore get() = storage.messageStore
|
||||||
private val workingMemory: WorkingMemoryStore get() = stores.workingMemory
|
private val workingMemory: WorkingMemoryStore get() = storage.workingMemoryStore
|
||||||
|
|
||||||
private val toolsByName: Map<String, NamedTool> = tools.associateBy { it.name }
|
private val toolsByName: Map<String, NamedTool> = tools.associateBy { it.name }
|
||||||
|
|
||||||
|
|||||||
@@ -38,28 +38,28 @@ import kotlin.time.Instant
|
|||||||
|
|
||||||
class ChatAgentTest {
|
class ChatAgentTest {
|
||||||
|
|
||||||
private lateinit var stores: SqliteStores
|
private lateinit var storage: pw.binom.agentik.storage.StorageBundle
|
||||||
private lateinit var fakeLlm: FakeLiteLlm
|
private lateinit var fakeLlm: FakeLiteLlm
|
||||||
|
|
||||||
@BeforeTest
|
@BeforeTest
|
||||||
fun setup() {
|
fun setup() {
|
||||||
stores = SqliteStores.inMemory()
|
storage = SqliteStores.inMemory().asBundle()
|
||||||
fakeLlm = FakeLiteLlm()
|
fakeLlm = FakeLiteLlm()
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterTest
|
@AfterTest
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
stores.close()
|
storage.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun newAgent(
|
private fun newAgent(
|
||||||
stores: SqliteStores = this.stores,
|
storage: pw.binom.agentik.storage.StorageBundle = this.storage,
|
||||||
llm: LiteLlm = this.fakeLlm,
|
llm: LiteLlm = this.fakeLlm,
|
||||||
tools: List<NamedTool> = emptyList(),
|
tools: List<NamedTool> = emptyList(),
|
||||||
skills: SkillCatalog = SkillCatalog.EMPTY,
|
skills: SkillCatalog = SkillCatalog.EMPTY,
|
||||||
): ChatAgent = ChatAgent(
|
): ChatAgent = ChatAgent(
|
||||||
id = "agentik",
|
id = "agentik",
|
||||||
stores = stores,
|
storage = storage,
|
||||||
llm = llm,
|
llm = llm,
|
||||||
llmConfig = LlmConfig(
|
llmConfig = LlmConfig(
|
||||||
backend = LlmBackend.OPENAI,
|
backend = LlmBackend.OPENAI,
|
||||||
@@ -75,7 +75,7 @@ class ChatAgentTest {
|
|||||||
val agent = newAgent()
|
val agent = newAgent()
|
||||||
val conv = agent.createConversation(temp = false) as ChatConversation
|
val conv = agent.createConversation(temp = false) as ChatConversation
|
||||||
|
|
||||||
val wm = stores.workingMemory.list(conv.id)
|
val wm = storage.workingMemoryStore.list(conv.id)
|
||||||
assertEquals(1, wm.size)
|
assertEquals(1, wm.size)
|
||||||
val first = wm[0]
|
val first = wm[0]
|
||||||
val sysEntry = first.entry as pw.binom.agentik.storage.WorkingMemoryEntry.System
|
val sysEntry = first.entry as pw.binom.agentik.storage.WorkingMemoryEntry.System
|
||||||
@@ -90,7 +90,7 @@ class ChatAgentTest {
|
|||||||
val agent = newAgent(skills = skills)
|
val agent = newAgent(skills = skills)
|
||||||
val conv = agent.createConversation(temp = false) as ChatConversation
|
val conv = agent.createConversation(temp = false) as ChatConversation
|
||||||
|
|
||||||
val system = stores.workingMemory.list(conv.id).first().entry
|
val system = storage.workingMemoryStore.list(conv.id).first().entry
|
||||||
as pw.binom.agentik.storage.WorkingMemoryEntry.System
|
as pw.binom.agentik.storage.WorkingMemoryEntry.System
|
||||||
assertTrue("be brief" in system.text)
|
assertTrue("be brief" in system.text)
|
||||||
assertTrue("## Навыки" in system.text)
|
assertTrue("## Навыки" in system.text)
|
||||||
@@ -147,7 +147,7 @@ class ChatAgentTest {
|
|||||||
val id = conv.id
|
val id = conv.id
|
||||||
|
|
||||||
// добавим сообщение, чтобы потом убедиться, что каскад сработал
|
// добавим сообщение, чтобы потом убедиться, что каскад сработал
|
||||||
stores.messages.append(
|
storage.messageStore.append(
|
||||||
pw.binom.agentik.storage.MessageRecord.UserMessage(
|
pw.binom.agentik.storage.MessageRecord.UserMessage(
|
||||||
id = "m1",
|
id = "m1",
|
||||||
conversationId = id,
|
conversationId = id,
|
||||||
@@ -157,8 +157,8 @@ class ChatAgentTest {
|
|||||||
)
|
)
|
||||||
assertTrue(agent.deleteConversation(id))
|
assertTrue(agent.deleteConversation(id))
|
||||||
assertNull(agent.getConversation(id))
|
assertNull(agent.getConversation(id))
|
||||||
assertNull(stores.conversations.get(id))
|
assertNull(storage.conversationStore.get(id))
|
||||||
assertEquals(emptyList(), stores.messages.listAll(id))
|
assertEquals(emptyList(), storage.messageStore.listAll(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -176,7 +176,7 @@ class ChatAgentTest {
|
|||||||
conv.send(listOf(Content.Text("hi")))
|
conv.send(listOf(Content.Text("hi")))
|
||||||
|
|
||||||
// user message записан в audit + working memory
|
// user message записан в audit + working memory
|
||||||
val msgs = stores.messages.listAll(conv.id)
|
val msgs = storage.messageStore.listAll(conv.id)
|
||||||
assertEquals(2, msgs.size)
|
assertEquals(2, msgs.size)
|
||||||
assertEquals("hi", (msgs[0] as pw.binom.agentik.storage.MessageRecord.UserMessage).content.let {
|
assertEquals("hi", (msgs[0] as pw.binom.agentik.storage.MessageRecord.UserMessage).content.let {
|
||||||
(it[0] as pw.binom.agentik.storage.Content.Text).body
|
(it[0] as pw.binom.agentik.storage.Content.Text).body
|
||||||
@@ -200,10 +200,10 @@ class ChatAgentTest {
|
|||||||
conv2.send(listOf(Content.Text("second user")))
|
conv2.send(listOf(Content.Text("second user")))
|
||||||
|
|
||||||
// Первая беседа должна иметь только свою систему + 1 user + 1 assistant
|
// Первая беседа должна иметь только свою систему + 1 user + 1 assistant
|
||||||
val wm1 = stores.workingMemory.list(conv1.id)
|
val wm1 = storage.workingMemoryStore.list(conv1.id)
|
||||||
assertEquals(3, wm1.size)
|
assertEquals(3, wm1.size)
|
||||||
// Вторая беседа — только своё
|
// Вторая беседа — только своё
|
||||||
val wm2 = stores.workingMemory.list(conv2.id)
|
val wm2 = storage.workingMemoryStore.list(conv2.id)
|
||||||
assertEquals(3, wm2.size)
|
assertEquals(3, wm2.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,12 +237,12 @@ class ChatAgentTest {
|
|||||||
fakeLlm.reply = "first reply"
|
fakeLlm.reply = "first reply"
|
||||||
conv.send(listOf(Content.Text("first user")))
|
conv.send(listOf(Content.Text("first user")))
|
||||||
// первый turn: WM = [system, user, assistant]
|
// первый turn: WM = [system, user, assistant]
|
||||||
assertEquals(3, stores.workingMemory.list(conv.id).size)
|
assertEquals(3, storage.workingMemoryStore.list(conv.id).size)
|
||||||
|
|
||||||
fakeLlm.reply = "second reply"
|
fakeLlm.reply = "second reply"
|
||||||
conv.send(listOf(Content.Text("second user")))
|
conv.send(listOf(Content.Text("second user")))
|
||||||
// второй turn: WM должен вырасти до [system, user, assistant, user, assistant]
|
// второй turn: WM должен вырасти до [system, user, assistant, user, assistant]
|
||||||
val wm = stores.workingMemory.list(conv.id)
|
val wm = storage.workingMemoryStore.list(conv.id)
|
||||||
assertEquals(5, wm.size)
|
assertEquals(5, wm.size)
|
||||||
// Длинно-живущий LiteConversation: один на ChatConversation, история
|
// Длинно-живущий LiteConversation: один на ChatConversation, история
|
||||||
// накапливается через sendStreamContents, без пересоздания.
|
// накапливается через sendStreamContents, без пересоздания.
|
||||||
@@ -303,7 +303,7 @@ class ChatAgentTest {
|
|||||||
assertTrue(events.any { it is ProtoEvent.Error && it.message == "boom from llm" }, "events=$events")
|
assertTrue(events.any { it is ProtoEvent.Error && it.message == "boom from llm" }, "events=$events")
|
||||||
assertTrue(events.any { it is ProtoEvent.End }, "events=$events")
|
assertTrue(events.any { it is ProtoEvent.End }, "events=$events")
|
||||||
|
|
||||||
val msgs = stores.messages.listAll(conv.id)
|
val msgs = storage.messageStore.listAll(conv.id)
|
||||||
assertEquals(2, msgs.size)
|
assertEquals(2, msgs.size)
|
||||||
assertIs<pw.binom.agentik.storage.MessageRecord.UserMessage>(msgs[0])
|
assertIs<pw.binom.agentik.storage.MessageRecord.UserMessage>(msgs[0])
|
||||||
val err = assertIs<pw.binom.agentik.storage.MessageRecord.Error>(msgs[1])
|
val err = assertIs<pw.binom.agentik.storage.MessageRecord.Error>(msgs[1])
|
||||||
@@ -336,7 +336,7 @@ class ChatAgentTest {
|
|||||||
sendJob.join()
|
sendJob.join()
|
||||||
|
|
||||||
// user сообщение в audit должно быть, assistant — нет (был отменён)
|
// user сообщение в audit должно быть, assistant — нет (был отменён)
|
||||||
val msgs = stores.messages.listAll(conv.id)
|
val msgs = storage.messageStore.listAll(conv.id)
|
||||||
assertEquals(1, msgs.size)
|
assertEquals(1, msgs.size)
|
||||||
assertIs<pw.binom.agentik.storage.MessageRecord.UserMessage>(msgs[0])
|
assertIs<pw.binom.agentik.storage.MessageRecord.UserMessage>(msgs[0])
|
||||||
}
|
}
|
||||||
@@ -344,12 +344,12 @@ class ChatAgentTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `temp conversation is not persisted across agent instances`() = runTest {
|
fun `temp conversation is not persisted across agent instances`() = runTest {
|
||||||
// Поднимаем file-backed БД, создаём temp-беседу
|
// Поднимаем file-backed БД, создаём temp-беседу
|
||||||
stores.close()
|
storage.close()
|
||||||
val dbPath = (System.getProperty("java.io.tmpdir") + "/agentik-test-${System.nanoTime()}.db")
|
val dbPath = (System.getProperty("java.io.tmpdir") + "/agentik-test-${System.nanoTime()}.db")
|
||||||
stores = SqliteStores.open(dbPath)
|
storage = SqliteStores.open(dbPath).asBundle()
|
||||||
val agent1 = ChatAgent(
|
val agent1 = ChatAgent(
|
||||||
id = "agentik",
|
id = "agentik",
|
||||||
stores = stores,
|
storage = storage,
|
||||||
llm = FakeLiteLlm().also { fakeLlm = it },
|
llm = FakeLiteLlm().also { fakeLlm = it },
|
||||||
llmConfig = LlmConfig(
|
llmConfig = LlmConfig(
|
||||||
backend = pw.binom.agentik.standalone.llm.LlmBackend.OPENAI,
|
backend = pw.binom.agentik.standalone.llm.LlmBackend.OPENAI,
|
||||||
@@ -362,11 +362,11 @@ class ChatAgentTest {
|
|||||||
assertNotNull(agent1.getConversation(tempId))
|
assertNotNull(agent1.getConversation(tempId))
|
||||||
|
|
||||||
// Переоткрываем БД — temp-беседа не должна пережить рестарт
|
// Переоткрываем БД — temp-беседа не должна пережить рестарт
|
||||||
stores.close()
|
storage.close()
|
||||||
stores = SqliteStores.open(dbPath)
|
storage = SqliteStores.open(dbPath).asBundle()
|
||||||
val agent2 = ChatAgent(
|
val agent2 = ChatAgent(
|
||||||
id = "agentik",
|
id = "agentik",
|
||||||
stores = stores,
|
storage = storage,
|
||||||
llm = fakeLlm,
|
llm = fakeLlm,
|
||||||
llmConfig = LlmConfig(
|
llmConfig = LlmConfig(
|
||||||
backend = pw.binom.agentik.standalone.llm.LlmBackend.OPENAI,
|
backend = pw.binom.agentik.standalone.llm.LlmBackend.OPENAI,
|
||||||
@@ -380,12 +380,12 @@ class ChatAgentTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `non-temp conversation persists across agent instances`() = runTest {
|
fun `non-temp conversation persists across agent instances`() = runTest {
|
||||||
stores.close()
|
storage.close()
|
||||||
val dbPath = (System.getProperty("java.io.tmpdir") + "/agentik-test-${System.nanoTime()}.db")
|
val dbPath = (System.getProperty("java.io.tmpdir") + "/agentik-test-${System.nanoTime()}.db")
|
||||||
stores = SqliteStores.open(dbPath)
|
storage = SqliteStores.open(dbPath).asBundle()
|
||||||
val agent1 = ChatAgent(
|
val agent1 = ChatAgent(
|
||||||
id = "agentik",
|
id = "agentik",
|
||||||
stores = stores,
|
storage = storage,
|
||||||
llm = FakeLiteLlm().also { fakeLlm = it },
|
llm = FakeLiteLlm().also { fakeLlm = it },
|
||||||
llmConfig = LlmConfig(
|
llmConfig = LlmConfig(
|
||||||
backend = pw.binom.agentik.standalone.llm.LlmBackend.OPENAI,
|
backend = pw.binom.agentik.standalone.llm.LlmBackend.OPENAI,
|
||||||
@@ -396,11 +396,11 @@ class ChatAgentTest {
|
|||||||
val conv = agent1.createConversation(temp = false)
|
val conv = agent1.createConversation(temp = false)
|
||||||
val id = conv.id
|
val id = conv.id
|
||||||
|
|
||||||
stores.close()
|
storage.close()
|
||||||
stores = SqliteStores.open(dbPath)
|
storage = SqliteStores.open(dbPath).asBundle()
|
||||||
val agent2 = ChatAgent(
|
val agent2 = ChatAgent(
|
||||||
id = "agentik",
|
id = "agentik",
|
||||||
stores = stores,
|
storage = storage,
|
||||||
llm = fakeLlm,
|
llm = fakeLlm,
|
||||||
llmConfig = LlmConfig(
|
llmConfig = LlmConfig(
|
||||||
backend = pw.binom.agentik.standalone.llm.LlmBackend.OPENAI,
|
backend = pw.binom.agentik.standalone.llm.LlmBackend.OPENAI,
|
||||||
|
|||||||
+14
-14
@@ -39,11 +39,11 @@ class ChatAgentToolsetsTest {
|
|||||||
|
|
||||||
private fun newAgent(
|
private fun newAgent(
|
||||||
toolsets: List<ToolsetContribution> = emptyList(),
|
toolsets: List<ToolsetContribution> = emptyList(),
|
||||||
): Pair<ChatAgent, SqliteStores> {
|
): Pair<ChatAgent, pw.binom.agentik.storage.StorageBundle> {
|
||||||
val stores = SqliteStores.inMemory()
|
val storage = SqliteStores.inMemory().asBundle()
|
||||||
val agent = ChatAgent(
|
val agent = ChatAgent(
|
||||||
id = "test-agent",
|
id = "test-agent",
|
||||||
stores = stores,
|
storage = storage,
|
||||||
llm = stubLlm(),
|
llm = stubLlm(),
|
||||||
llmConfig = LlmConfig(
|
llmConfig = LlmConfig(
|
||||||
backend = pw.binom.agentik.standalone.llm.LlmBackend.GOOGLE,
|
backend = pw.binom.agentik.standalone.llm.LlmBackend.GOOGLE,
|
||||||
@@ -52,12 +52,12 @@ class ChatAgentToolsetsTest {
|
|||||||
),
|
),
|
||||||
toolsets = toolsets,
|
toolsets = toolsets,
|
||||||
)
|
)
|
||||||
return agent to stores
|
return agent to storage
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `default (empty toolsets) does not register enable or disable tools`() {
|
fun `default (empty toolsets) does not register enable or disable tools`() {
|
||||||
val (agent, stores) = newAgent()
|
val (agent, storage) = newAgent()
|
||||||
try {
|
try {
|
||||||
// Проверяем через allTools-эквивалент: вызываем enable_toolset
|
// Проверяем через allTools-эквивалент: вызываем enable_toolset
|
||||||
// и ожидаем что он не найден — это значит тул не зарегистрирован.
|
// и ожидаем что он не найден — это значит тул не зарегистрирован.
|
||||||
@@ -66,7 +66,7 @@ class ChatAgentToolsetsTest {
|
|||||||
val prompt = (conv as ChatConversation).let { it.systemPromptForTest() }
|
val prompt = (conv as ChatConversation).let { it.systemPromptForTest() }
|
||||||
assertFalse(prompt.contains("## Toolsets"), "toolsets section must NOT appear when toolsets empty")
|
assertFalse(prompt.contains("## Toolsets"), "toolsets section must NOT appear when toolsets empty")
|
||||||
assertFalse(prompt.contains("enable_toolset"), "enable_toolset must NOT be mentioned when toolsets empty")
|
assertFalse(prompt.contains("enable_toolset"), "enable_toolset must NOT be mentioned when toolsets empty")
|
||||||
} finally { stores.close() }
|
} finally { storage.close() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -83,7 +83,7 @@ class ChatAgentToolsetsTest {
|
|||||||
tools = listOf(ToolsetContribution.ToolEntry("fetch_url", stubTool("fetch_url"))),
|
tools = listOf(ToolsetContribution.ToolEntry("fetch_url", stubTool("fetch_url"))),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val (agent, stores) = newAgent(toolsets = toolsets)
|
val (agent, storage) = newAgent(toolsets = toolsets)
|
||||||
try {
|
try {
|
||||||
val conv = agent.createConversation(temp = true) as ChatConversation
|
val conv = agent.createConversation(temp = true) as ChatConversation
|
||||||
val prompt = conv.systemPromptForTest()
|
val prompt = conv.systemPromptForTest()
|
||||||
@@ -92,7 +92,7 @@ class ChatAgentToolsetsTest {
|
|||||||
assertTrue(prompt.contains("- web — fetch and parse web pages"))
|
assertTrue(prompt.contains("- web — fetch and parse web pages"))
|
||||||
// Оба тула — в списке allTools (через system prompt проверяем только prompt;
|
// Оба тула — в списке allTools (через system prompt проверяем только prompt;
|
||||||
// наличие тулов проверим отдельно — см. ниже).
|
// наличие тулов проверим отдельно — см. ниже).
|
||||||
} finally { stores.close() }
|
} finally { storage.close() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -105,7 +105,7 @@ class ChatAgentToolsetsTest {
|
|||||||
tools = listOf(ToolsetContribution.ToolEntry("resize_image", mediaTool)),
|
tools = listOf(ToolsetContribution.ToolEntry("resize_image", mediaTool)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val (agent, stores) = newAgent(toolsets = toolsets)
|
val (agent, storage) = newAgent(toolsets = toolsets)
|
||||||
try {
|
try {
|
||||||
val conv = agent.createConversation(temp = true) as ChatConversation
|
val conv = agent.createConversation(temp = true) as ChatConversation
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ class ChatAgentToolsetsTest {
|
|||||||
conv.dispatchToolForTest("resize_image", "{}")
|
conv.dispatchToolForTest("resize_image", "{}")
|
||||||
}
|
}
|
||||||
assertEquals("image-resized-100x100", resized)
|
assertEquals("image-resized-100x100", resized)
|
||||||
} finally { stores.close() }
|
} finally { storage.close() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -133,7 +133,7 @@ class ChatAgentToolsetsTest {
|
|||||||
tools = listOf(ToolsetContribution.ToolEntry("resize_image", mediaTool)),
|
tools = listOf(ToolsetContribution.ToolEntry("resize_image", mediaTool)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val (agent, stores) = newAgent(toolsets = toolsets)
|
val (agent, storage) = newAgent(toolsets = toolsets)
|
||||||
try {
|
try {
|
||||||
val conv = agent.createConversation(temp = true) as ChatConversation
|
val conv = agent.createConversation(temp = true) as ChatConversation
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ class ChatAgentToolsetsTest {
|
|||||||
conv.dispatchToolForTest("resize_image", "{}")
|
conv.dispatchToolForTest("resize_image", "{}")
|
||||||
}
|
}
|
||||||
assertEquals("auto-activated-and-ran", result)
|
assertEquals("auto-activated-and-ran", result)
|
||||||
} finally { stores.close() }
|
} finally { storage.close() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -158,7 +158,7 @@ class ChatAgentToolsetsTest {
|
|||||||
tools = listOf(ToolsetContribution.ToolEntry("resize_image", stubTool("resize_image"))),
|
tools = listOf(ToolsetContribution.ToolEntry("resize_image", stubTool("resize_image"))),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val (agent, stores) = newAgent(toolsets = toolsets)
|
val (agent, storage) = newAgent(toolsets = toolsets)
|
||||||
try {
|
try {
|
||||||
val conv = agent.createConversation(temp = true) as ChatConversation
|
val conv = agent.createConversation(temp = true) as ChatConversation
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ class ChatAgentToolsetsTest {
|
|||||||
conv.dispatchToolForTest("disable_toolset", """{"name":"media"}""")
|
conv.dispatchToolForTest("disable_toolset", """{"name":"media"}""")
|
||||||
}
|
}
|
||||||
assertEquals("Toolset 'media' deactivated.", disabledAgain)
|
assertEquals("Toolset 'media' deactivated.", disabledAgain)
|
||||||
} finally { stores.close() }
|
} finally { storage.close() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,18 +39,18 @@ import kotlinx.coroutines.flow.asSharedFlow
|
|||||||
*/
|
*/
|
||||||
class CompactionTest {
|
class CompactionTest {
|
||||||
|
|
||||||
private lateinit var stores: SqliteStores
|
private lateinit var storage: pw.binom.agentik.storage.StorageBundle
|
||||||
private lateinit var fakeLlm: FakeLiteLlm
|
private lateinit var fakeLlm: FakeLiteLlm
|
||||||
|
|
||||||
@BeforeTest
|
@BeforeTest
|
||||||
fun setup() {
|
fun setup() {
|
||||||
stores = SqliteStores.inMemory()
|
storage = SqliteStores.inMemory().asBundle()
|
||||||
fakeLlm = FakeLiteLlm()
|
fakeLlm = FakeLiteLlm()
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterTest
|
@AfterTest
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
stores.close()
|
storage.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun newAgent(
|
private fun newAgent(
|
||||||
@@ -62,7 +62,7 @@ class CompactionTest {
|
|||||||
val reviewer = if (memoryStore != null) KeywordMdReviewer() else null
|
val reviewer = if (memoryStore != null) KeywordMdReviewer() else null
|
||||||
return ChatAgent(
|
return ChatAgent(
|
||||||
id = "test",
|
id = "test",
|
||||||
stores = stores,
|
storage = storage,
|
||||||
llm = fakeLlm,
|
llm = fakeLlm,
|
||||||
llmConfig = LlmConfig(
|
llmConfig = LlmConfig(
|
||||||
backend = LlmBackend.OPENAI,
|
backend = LlmBackend.OPENAI,
|
||||||
@@ -86,7 +86,7 @@ class CompactionTest {
|
|||||||
repeat(10) {
|
repeat(10) {
|
||||||
conv.send(listOf(ProtoContent.Text("turn $it: ${"x".repeat(200)}")))
|
conv.send(listOf(ProtoContent.Text("turn $it: ${"x".repeat(200)}")))
|
||||||
}
|
}
|
||||||
val wm = stores.workingMemory.list(conv.id)
|
val wm = storage.workingMemoryStore.list(conv.id)
|
||||||
// Без compaction все ходы остаются в памяти (System + 10 user/assistant = 21 строк).
|
// Без compaction все ходы остаются в памяти (System + 10 user/assistant = 21 строк).
|
||||||
val summaries = wm.filter { it.entry is pw.binom.agentik.storage.WorkingMemoryEntry.Summary }
|
val summaries = wm.filter { it.entry is pw.binom.agentik.storage.WorkingMemoryEntry.Summary }
|
||||||
assertEquals(0, summaries.size, "compaction must not run without contextWindow")
|
assertEquals(0, summaries.size, "compaction must not run without contextWindow")
|
||||||
@@ -98,7 +98,7 @@ class CompactionTest {
|
|||||||
val agent = newAgent(contextWindow = 10, compactor = null)
|
val agent = newAgent(contextWindow = 10, compactor = null)
|
||||||
val conv = agent.createConversation(temp = false) as ChatConversation
|
val conv = agent.createConversation(temp = false) as ChatConversation
|
||||||
conv.send(listOf(ProtoContent.Text("first")))
|
conv.send(listOf(ProtoContent.Text("first")))
|
||||||
val wm = stores.workingMemory.list(conv.id)
|
val wm = storage.workingMemoryStore.list(conv.id)
|
||||||
// System + User + Assistant = 3. Без compactor — никаких Summary.
|
// System + User + Assistant = 3. Без compactor — никаких Summary.
|
||||||
val summaries = wm.filter { it.entry is pw.binom.agentik.storage.WorkingMemoryEntry.Summary }
|
val summaries = wm.filter { it.entry is pw.binom.agentik.storage.WorkingMemoryEntry.Summary }
|
||||||
assertEquals(0, summaries.size, "no compaction runs without compactor")
|
assertEquals(0, summaries.size, "no compaction runs without compactor")
|
||||||
@@ -117,7 +117,7 @@ class CompactionTest {
|
|||||||
// Compactor должен был быть вызван хотя бы раз.
|
// Compactor должен был быть вызван хотя бы раз.
|
||||||
assertTrue(compactor.calls > 0, "compactor must be called at least once when above threshold")
|
assertTrue(compactor.calls > 0, "compactor must be called at least once when above threshold")
|
||||||
// В working memory должна появиться Summary.
|
// В working memory должна появиться Summary.
|
||||||
val wm = stores.workingMemory.list(conv.id)
|
val wm = storage.workingMemoryStore.list(conv.id)
|
||||||
val summaries = wm.filter { it.entry is pw.binom.agentik.storage.WorkingMemoryEntry.Summary }
|
val summaries = wm.filter { it.entry is pw.binom.agentik.storage.WorkingMemoryEntry.Summary }
|
||||||
assertTrue(summaries.isNotEmpty(), "at least one Summary entry should be present after compaction")
|
assertTrue(summaries.isNotEmpty(), "at least one Summary entry should be present after compaction")
|
||||||
// Summary-текст — то, что вернул наш compactor.
|
// Summary-текст — то, что вернул наш compactor.
|
||||||
@@ -157,7 +157,7 @@ class CompactionTest {
|
|||||||
conv.send(listOf(ProtoContent.Text("second turn")))
|
conv.send(listOf(ProtoContent.Text("second turn")))
|
||||||
conv.send(listOf(ProtoContent.Text("third turn — long content ${"y".repeat(150)}")))
|
conv.send(listOf(ProtoContent.Text("third turn — long content ${"y".repeat(150)}")))
|
||||||
|
|
||||||
val wm = stores.workingMemory.list(conv.id)
|
val wm = storage.workingMemoryStore.list(conv.id)
|
||||||
// Должны быть: System + хотя бы один Summary + последние KEEP_RECENT_TURNS ходов.
|
// Должны быть: System + хотя бы один Summary + последние KEEP_RECENT_TURNS ходов.
|
||||||
// KEEP_RECENT_TURNS = 4 → user/assistant последних двух ходов (third + second) могут быть не тронуты.
|
// KEEP_RECENT_TURNS = 4 → user/assistant последних двух ходов (third + second) могут быть не тронуты.
|
||||||
val userAssistantCount = wm.count {
|
val userAssistantCount = wm.count {
|
||||||
|
|||||||
@@ -47,13 +47,13 @@ import kotlin.time.Instant
|
|||||||
*/
|
*/
|
||||||
class MemoryWiringTest {
|
class MemoryWiringTest {
|
||||||
|
|
||||||
private lateinit var stores: SqliteStores
|
private lateinit var storage: pw.binom.agentik.storage.StorageBundle
|
||||||
private lateinit var fakeLlm: FakeLiteLlm
|
private lateinit var fakeLlm: FakeLiteLlm
|
||||||
private lateinit var root: Path
|
private lateinit var root: Path
|
||||||
|
|
||||||
@BeforeTest
|
@BeforeTest
|
||||||
fun setup() {
|
fun setup() {
|
||||||
stores = SqliteStores.inMemory()
|
storage = SqliteStores.inMemory().asBundle()
|
||||||
fakeLlm = FakeLiteLlm()
|
fakeLlm = FakeLiteLlm()
|
||||||
root = Path(SystemTemporaryDirectory.toString(), "agentik-mem-${java.util.UUID.randomUUID()}")
|
root = Path(SystemTemporaryDirectory.toString(), "agentik-mem-${java.util.UUID.randomUUID()}")
|
||||||
SystemFileSystem.createDirectories(root, mustCreate = true)
|
SystemFileSystem.createDirectories(root, mustCreate = true)
|
||||||
@@ -61,7 +61,7 @@ class MemoryWiringTest {
|
|||||||
|
|
||||||
@AfterTest
|
@AfterTest
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
stores.close()
|
storage.close()
|
||||||
runCatching { SystemFileSystem.delete(root, mustExist = false) }
|
runCatching { SystemFileSystem.delete(root, mustExist = false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ class MemoryWiringTest {
|
|||||||
reviewer: MemoryReviewer,
|
reviewer: MemoryReviewer,
|
||||||
): ChatAgent = ChatAgent(
|
): ChatAgent = ChatAgent(
|
||||||
id = "agentik",
|
id = "agentik",
|
||||||
stores = stores,
|
storage = storage,
|
||||||
llm = fakeLlm,
|
llm = fakeLlm,
|
||||||
llmConfig = LlmConfig(
|
llmConfig = LlmConfig(
|
||||||
backend = LlmBackend.OPENAI,
|
backend = LlmBackend.OPENAI,
|
||||||
@@ -88,7 +88,7 @@ class MemoryWiringTest {
|
|||||||
val system = openMdMemorySystem(root)
|
val system = openMdMemorySystem(root)
|
||||||
val agent = newAgent(system.store, system.prefetcher, system.reviewer)
|
val agent = newAgent(system.store, system.prefetcher, system.reviewer)
|
||||||
val conv = agent.createConversation(temp = false)
|
val conv = agent.createConversation(temp = false)
|
||||||
val wm = stores.workingMemory.list(conv.id)
|
val wm = storage.workingMemoryStore.list(conv.id)
|
||||||
val sysRow = wm.first { it.entry is WorkingMemoryEntry.System }
|
val sysRow = wm.first { it.entry is WorkingMemoryEntry.System }
|
||||||
val text = (sysRow.entry as WorkingMemoryEntry.System).text
|
val text = (sysRow.entry as WorkingMemoryEntry.System).text
|
||||||
assertTrue(text.contains(MemorySystemGuidance.MEMORY_GUIDANCE.take(80)),
|
assertTrue(text.contains(MemorySystemGuidance.MEMORY_GUIDANCE.take(80)),
|
||||||
@@ -102,7 +102,7 @@ class MemoryWiringTest {
|
|||||||
val soulBody = "I am a helpful test persona. I always answer in one short line."
|
val soulBody = "I am a helpful test persona. I always answer in one short line."
|
||||||
val agent = ChatAgent(
|
val agent = ChatAgent(
|
||||||
id = "agentik",
|
id = "agentik",
|
||||||
stores = stores,
|
storage = storage,
|
||||||
llm = fakeLlm,
|
llm = fakeLlm,
|
||||||
llmConfig = LlmConfig(
|
llmConfig = LlmConfig(
|
||||||
backend = LlmBackend.OPENAI,
|
backend = LlmBackend.OPENAI,
|
||||||
@@ -112,7 +112,7 @@ class MemoryWiringTest {
|
|||||||
soulBody = soulBody,
|
soulBody = soulBody,
|
||||||
)
|
)
|
||||||
val conv = agent.createConversation(temp = false)
|
val conv = agent.createConversation(temp = false)
|
||||||
val wm = stores.workingMemory.list(conv.id)
|
val wm = storage.workingMemoryStore.list(conv.id)
|
||||||
val sysRow = wm.first { it.entry is WorkingMemoryEntry.System }
|
val sysRow = wm.first { it.entry is WorkingMemoryEntry.System }
|
||||||
val text = (sysRow.entry as WorkingMemoryEntry.System).text
|
val text = (sysRow.entry as WorkingMemoryEntry.System).text
|
||||||
assertTrue(text.startsWith(soulBody),
|
assertTrue(text.startsWith(soulBody),
|
||||||
@@ -126,7 +126,7 @@ class MemoryWiringTest {
|
|||||||
fun `soul body not added when null`() = runBlocking {
|
fun `soul body not added when null`() = runBlocking {
|
||||||
val agent = ChatAgent(
|
val agent = ChatAgent(
|
||||||
id = "agentik",
|
id = "agentik",
|
||||||
stores = stores,
|
storage = storage,
|
||||||
llm = fakeLlm,
|
llm = fakeLlm,
|
||||||
llmConfig = LlmConfig(
|
llmConfig = LlmConfig(
|
||||||
backend = LlmBackend.OPENAI,
|
backend = LlmBackend.OPENAI,
|
||||||
@@ -135,7 +135,7 @@ class MemoryWiringTest {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val conv = agent.createConversation(temp = false)
|
val conv = agent.createConversation(temp = false)
|
||||||
val wm = stores.workingMemory.list(conv.id)
|
val wm = storage.workingMemoryStore.list(conv.id)
|
||||||
val sysRow = wm.first { it.entry is WorkingMemoryEntry.System }
|
val sysRow = wm.first { it.entry is WorkingMemoryEntry.System }
|
||||||
val text = (sysRow.entry as WorkingMemoryEntry.System).text
|
val text = (sysRow.entry as WorkingMemoryEntry.System).text
|
||||||
assertTrue(text.startsWith("be brief"),
|
assertTrue(text.startsWith("be brief"),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
|
|||||||
import pw.binom.agentik.storage.ConversationStore
|
import pw.binom.agentik.storage.ConversationStore
|
||||||
import pw.binom.agentik.storage.MessageStore
|
import pw.binom.agentik.storage.MessageStore
|
||||||
import pw.binom.agentik.storage.ReflectionStore
|
import pw.binom.agentik.storage.ReflectionStore
|
||||||
|
import pw.binom.agentik.storage.StorageBundle
|
||||||
import pw.binom.agentik.storage.sqlite.SqliteReflectionStore
|
import pw.binom.agentik.storage.sqlite.SqliteReflectionStore
|
||||||
import pw.binom.agentik.storage.WorkingMemoryStore
|
import pw.binom.agentik.storage.WorkingMemoryStore
|
||||||
|
|
||||||
@@ -21,6 +22,18 @@ class SqliteStores private constructor(
|
|||||||
val reflections: ReflectionStore,
|
val reflections: ReflectionStore,
|
||||||
) : AutoCloseable {
|
) : AutoCloseable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удобный агрегатор — превращает SqliteStores в [StorageBundle] для передачи
|
||||||
|
* в агенты, которые работают через общий контракт хранения (ChatAgent после
|
||||||
|
* commit 6 в :standalone принимает [StorageBundle], не [SqliteStores]).
|
||||||
|
*/
|
||||||
|
fun asBundle(): StorageBundle = StorageBundle(
|
||||||
|
conversationStore = conversations,
|
||||||
|
messageStore = messages,
|
||||||
|
workingMemoryStore = workingMemory,
|
||||||
|
reflectionStore = reflections,
|
||||||
|
)
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
conversations.close()
|
conversations.close()
|
||||||
messages.close()
|
messages.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user