agent-toolsets: SystemPromptToolsetSection + интеграция в ChatAgent
Добавлен SystemPromptToolsetSection — рендер markdown-секции для system prompt. Контракт: - toolsets пустой → null (секция не добавляется, агент не знает о механике) - иначе → краткое описание концепции + список 'name — description' для активных и неактивных (одинаковый формат per design contract) - auto-activation НЕ упоминается в промпте (только в dispatch) Интеграция в ChatAgent: - Добавлен параметр toolsets: List<ToolsetContribution> = emptyList() - При пустом списке — enable_toolset/disable_toolset НЕ регистрируются, секция в system prompt НЕ появляется (полная невидимость per A1-α) - При непустом — тулы регистрируются, секция добавляется - ToolsetRegistry + ToolsetDispatchPolicy создаются per-agent (один реестр на все диалоги — состояние 'активные тулсеты' общее) Интеграция в ChatConversation: - Новый параметр toolsetDispatch: ToolsetDispatchPolicy? = null - runToolAndPersist: если задан — вызов идёт через policy (auto-activate неактивных тулсетов, fallback в base dispatcher для плоских тулов) - Иначе — старое поведение через toolsByName Тесты: - 7 новых в :agent-toolsets (SystemPromptToolsetSection): пустые списки, только активные, только неактивные, оба, проверка отсутствия auto-activation упоминания, registry-based рендер, пустой реестр - 5 новых в :standalone (ChatAgentToolsetsTest): default (пустой) — нет тулов и секции; non-empty — тулы и секция есть; enable_toolset активирует; вызов тула из неактивного тулсета — auto-activate; disable_toolset снимает из active set (но auto-activate на следующем вызове — by design) Tests: 340/340 green (335 ранее + 5 новых ChatAgent integration)
This commit is contained in:
+54
@@ -0,0 +1,54 @@
|
||||
package pw.binom.agentik.toolsets
|
||||
|
||||
/**
|
||||
* Markdown-секция для system prompt, описывающая доступные тулсеты.
|
||||
*
|
||||
* Контракт (зафиксирован в дизайн-доке `docs/TOOLSETS-PLAN.md`):
|
||||
* - **Если toolsets пустой** → `null` (секция не добавляется, агент не знает
|
||||
* о механике toolsets вообще; тулы enable_toolset/disable_toolset тоже
|
||||
* не регистрируются — полная невидимость).
|
||||
* - **Иначе** → короткое описание концепции + список всех тулсетов
|
||||
* в формате `name — description`, активные и неактивные одинаково
|
||||
* (модель видит за что каждый отвечает).
|
||||
*
|
||||
* Auto-activation НЕ упоминается в prompt — только в dispatch (если модель
|
||||
* случайно вызвала тул из выключенного тулсета, диспетчер сам активирует).
|
||||
* Это чтобы не давать модели ложную опцию "не буду enable, а просто вызову".
|
||||
*/
|
||||
object SystemPromptToolsetSection {
|
||||
|
||||
fun render(
|
||||
active: List<ToolsetContribution>,
|
||||
inactive: List<ToolsetContribution>,
|
||||
): String? {
|
||||
if (active.isEmpty() && inactive.isEmpty()) return null
|
||||
return buildString {
|
||||
appendLine("## Toolsets")
|
||||
appendLine()
|
||||
appendLine("Toolsets group related tools. Use enable_toolset to activate one; its tools become available. Use disable_toolset to deactivate.")
|
||||
appendLine()
|
||||
if (active.isNotEmpty()) {
|
||||
appendLine("Active:")
|
||||
for (c in active) appendLine("- ${c.name} — ${c.description}")
|
||||
appendLine()
|
||||
}
|
||||
if (inactive.isNotEmpty()) {
|
||||
appendLine("Inactive:")
|
||||
for (c in inactive) appendLine("- ${c.name} — ${c.description}")
|
||||
appendLine()
|
||||
}
|
||||
}.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: рендер по [ToolsetRegistry] (синхронный — без activeTools(),
|
||||
* только имена и описания).
|
||||
*/
|
||||
fun render(registry: ToolsetRegistry, activeNames: Set<String>): String? {
|
||||
val all = registry.all()
|
||||
if (all.isEmpty()) return null
|
||||
val active = all.filter { it.name in activeNames }
|
||||
val inactive = all.filter { it.name !in activeNames }
|
||||
return render(active, inactive)
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package pw.binom.agentik.toolsets
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import pw.binom.litert.LiteTool
|
||||
|
||||
class SystemPromptToolsetSectionTest {
|
||||
|
||||
private fun stub(name: String, desc: String): ToolsetContribution = ToolsetContribution(
|
||||
name = name,
|
||||
description = desc,
|
||||
tools = emptyList(), // prompt section не зависит от tools
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `empty lists return null - section is omitted entirely`() {
|
||||
assertNull(SystemPromptToolsetSection.render(emptyList(), emptyList()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `only active present - omits inactive header`() {
|
||||
val section = SystemPromptToolsetSection.render(
|
||||
active = listOf(stub("a", "first toolset")),
|
||||
inactive = emptyList(),
|
||||
)
|
||||
assertEquals(true, section!!.contains("## Toolsets"))
|
||||
assertEquals(true, section.contains("Active:"))
|
||||
assertEquals(true, section.contains("- a — first toolset"))
|
||||
assertEquals(false, section.contains("Inactive:"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `only inactive present - omits active header`() {
|
||||
val section = SystemPromptToolsetSection.render(
|
||||
active = emptyList(),
|
||||
inactive = listOf(stub("b", "second toolset")),
|
||||
)
|
||||
assertEquals(true, section!!.contains("## Toolsets"))
|
||||
assertEquals(true, section.contains("Inactive:"))
|
||||
assertEquals(true, section.contains("- b — second toolset"))
|
||||
assertEquals(false, section.contains("\nActive:"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `both active and inactive - renders both blocks`() {
|
||||
val section = SystemPromptToolsetSection.render(
|
||||
active = listOf(stub("a", "first")),
|
||||
inactive = listOf(stub("b", "second"), stub("c", "third")),
|
||||
)
|
||||
assertEquals(true, section!!.contains("- a — first"))
|
||||
assertEquals(true, section.contains("- b — second"))
|
||||
assertEquals(true, section.contains("- c — third"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `does not mention auto-activation - per design contract`() {
|
||||
val section = SystemPromptToolsetSection.render(
|
||||
active = emptyList(),
|
||||
inactive = listOf(stub("a", "x")),
|
||||
)!!
|
||||
// Дизайн-док: auto-activation НЕ в промпте (только в dispatch)
|
||||
assertEquals(false, section.contains("auto", ignoreCase = true))
|
||||
assertEquals(false, section.contains("автоматическ", ignoreCase = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `registry-based render filters by active names`() = runTest {
|
||||
val reg = ToolsetRegistry(listOf(
|
||||
stub("a", "first"),
|
||||
stub("b", "second"),
|
||||
))
|
||||
reg.activate("a")
|
||||
val section = SystemPromptToolsetSection.render(reg, setOf("a"))
|
||||
assertEquals(true, section!!.contains("- a — first"))
|
||||
assertEquals(true, section.contains("- b — second"))
|
||||
assertEquals(true, section.contains("Active:"))
|
||||
assertEquals(true, section.contains("Inactive:"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty registry renders null`() = runTest {
|
||||
val reg = ToolsetRegistry.empty()
|
||||
assertNull(SystemPromptToolsetSection.render(reg, setOf()))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user