diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f585e358f..c8d98c8723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,79 @@ +# 0.7.1 +> Published 17 March 2026 + +## Major Features + +- **Java API**: Introduced comprehensive Java interoperability across the framework: + - Java API for creating and running agents from pure Java projects (#1185) + - Builder-based Java API for graph strategies (#1581, #1617, #1366) + - Java-friendly API for `AIAgentStorage` with JVM-specific methods (#1600) + - Blocking API builders for `PromptExecutor` and `LLMClient` for Java (#1555, #1604) + - Jackson as the default serializer for Java API (#1630) + - Weave and Langfuse integrations now available from Java (#1616) + - Centralized Java/Kotlin time conversion utilities (`TimeUtils`, `toKotlinDuration`, etc.) (#1620) +- **Spring AI Integration**: Added two new Spring Boot starters (`koog-spring-ai-starter-model-chat` and `koog-spring-ai-starter-model-embedding`) to integrate Spring AI `ChatModel` and `EmbeddingModel` implementations as Koog LLM backends, enabling support for the wide range of providers available in Spring AI ([KG-109](https://youtrack.jetbrains.com/issue/KG-109), #1587) +- **Chat Memory**: Introduced persistent chat memory with multiple storage backend options: + - Core `ChatMemory` feature and `ChatHistoryProvider` abstraction (#1511) + - Exposed-ORM based providers for PostgreSQL, MySQL, and H2 (#1584) + - Pure JDBC `ChatHistoryProvider` for PostgreSQL, MySQL, and H2 with no ORM dependency (#1597) + - JDBC-based `PersistenceStorageProvider` (#1612) +- **Long-term Memory**: Added `LongTermMemory` feature that augments prompts with relevant memory records from storage and extracts/ingests new memories from agent conversations (#1490) +- **Library-Agnostic Serialization API**: Introduced a `JSONSerializer` abstraction to support pluggable serialization libraries. Two implementations provided: `KotlinxSerializer` (default) and the new `JacksonSerializer` in a separate `serialization-jackson` module. Tools API migrated to this new abstraction (#1588) + +## Improvements + +- **OpenTelemetry**: + - Added OpenTelemetry support for functional agent pipelines ([KG-677](https://youtrack.jetbrains.com/issue/KG-677), #1447) + - Added OpenTelemetry spans for MCP tool calls (#1421) +- **Planner improvements**: + - Added `AIAgentPlannerContext` and `AIAgentFunctionalContextBase` for better context hierarchy and planner-specific APIs (#1480) + - Added planner-specific pipeline interceptors: `onPlanCreationStarting/Completed`, `onStepExecutionStarting/Completed`, `onPlanCompletionEvaluationStarting/Completed` ([KG-672](https://youtrack.jetbrains.com/issue/KG-672), #1550) + - GOAP strategies now have typed input/output and a dedicated `GoapAgentState` (#1498) +- **OpenRouter embedding support**: Implemented `LLMEmbeddingProvider` for OpenRouter, enabling access to 21+ embedding models ([KG-659](https://youtrack.jetbrains.com/issue/KG-659), #1398) +- **Swift Package Manager support**: Added XCFramework build and distribution infrastructure for iOS/macOS development via SPM ([KG-682](https://youtrack.jetbrains.com/issue/KG-682), #1485) + +## New LLM Models + +- **Anthropic Claude Opus 4.6**: Added support via Anthropic and Bedrock executors (#1513) +- **Google Gemini 3 Flash Preview**: New model with extended capabilities and high-speed processing (#1621) +- **OpenAI GPT-5.x series**: Added GPT-5.1-Codex-Max, GPT-5.2-Codex, GPT-5.3-Codex, GPT-5.4, and GPT-5.4-Pro (#1595) +- **Moonshot Kimi K2 Thinking**: Added support via the Bedrock Converse API (#1436) +- **Ollama thinking support**: Added `think=true` request parameter and streaming reasoning delta support for Ollama models (#1532) + +## Bug Fixes + +- **Persistence checkpoints**: Fixed last successful node being re-executed when restoring from a checkpoint; changed `lastInput` to `lastOutput` in checkpoint structure (#1308) +- **Ollama streaming**: Fixed Ollama client to use `preparePost(...).execute` for proper streaming instead of buffering the full response (#1497) +- **OpenRouter streaming**: Fixed missing `reasoning` and `reasoningDetails` fields in `OpenRouterStreamDelta` causing deserialization errors (#1504) +- **Dashscope streaming**: Fixed tool call argument merging for streaming responses in `DashscopeLLMClient` ([KG-658](https://youtrack.jetbrains.com/issue/KG-658), #1590) +- **`agents-ext` dependency leak**: Moved `agents-ext` from `commonMain api` to `jvmTest implementation` in `agents-test` to prevent transitive compile-time dependency leakage (#1506) +- **Streaming exception handling**: `executeStreaming` now properly propagates exceptions from LLM clients and requires `StreamFrame.End` to signal stream completion ([KG-550](https://youtrack.jetbrains.com/issue/KG-550), #1580) +- **Debugger feature**: Extended to support functional agents in addition to graph-based agents by dispatching appropriate strategy starting events ([KG-741](https://youtrack.jetbrains.com/issue/KG-741), #1637) + +## Breaking Changes + +- **Serialization API**: All `encode`/`decode` methods in `Tool` now accept a second `JSONSerializer` parameter. Automatic `ToolDescriptor` generation for primitive argument types (`Tool`) is no longer supported without a custom descriptor. `AIAgentFeature.createInitialConfig` now takes an `agentConfig: AIAgentConfig` parameter. JSON types in pipeline events changed from `kotlinx.serialization` to `ai.koog.serialization` (#1588) +- **`TypeToken` replaces `KType`**: Nodes and agent features now work with `ai.koog.serialization.TypeToken` instead of `kotlin.reflect.KType`. All `typeOf()` usages should be replaced with `typeToken()` (#1581) +- **Global JSON schema registries removed**: `RegisteredStandardJsonSchemaGenerators` and `RegisteredBasicJsonSchemaGenerators` removed. `getStructuredRequest` and `StructureFixingParser` moved to `ai.koog.prompt.executor.model` package ([KG-698](https://youtrack.jetbrains.com/issue/KG-698), #1517) +- **`LLMDescription.description` renamed to `value`**: The `description` field of `LLMDescription` has been renamed to `value` for Java compatibility (#1607) +- Deprecated `kotlinx.datetime` imports replaced with `kotlin.time` equivalents (`Clock`, `Instant`) (#1533) +- **Retired Anthropic/Bedrock models**: Removed `Sonnet_3_7`, `Haiku_3_5`, `Sonnet_3_5`, and `Opus_3` from Anthropic models; removed several AI21, Bedrock, and legacy Anthropic models. `Haiku_3` marked as deprecated (#1526) + +## Documentation + +- Added documentation for Java API and Java examples (#1610) +- Added documentation for Spring AI integration ([KG-109](https://youtrack.jetbrains.com/issue/KG-109), #1627) +- Added documentation for custom feature creation (#1295) +- Reworked Getting Started, agent types, and Chat Memory tutorials (#1349, #1552) +- Improved Prompts and Planner agent documentation (#1302, #1301) +- Added nightly builds documentation (#1433) + +## Examples + +- Added Java example for JavaOne 2026 (#1641) +- Added full Spring Boot Java API example (#1350) +- Added example for calling a Koog agent from JavaScript code, including browser (TypeScript webapp) and Node.js usage with `AbortSignal` support (#1500) + # 0.6.4 > Published 4 March 2026 diff --git a/README.md b/README.md index 4357b33a4f..cba9ddb52c 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ Currently, the framework supports the JVM, JS, WasmJS and iOS targets. ### Requirements - JDK 17 or higher is required to use the framework on JVM. -- kotlinx-coroutines 1.10.2 and kotlinx-serialization 1.8.1 versions should be set explicitly in existing projects. Please check the [libs.versions.toml](gradle/libs.versions.toml) to know more about the Koog dependencies. +- Kotlin 2.3.10 or higher should be set explicitly in existing projects. Please check the [libs.versions.toml](gradle/libs.versions.toml) to know more about Kotlin dependencies (currently it uses kotlinx-coroutines 1.10.2, kotlinx-serialization 1.10.0 and kotlinx-datetime 0.7.1) ### Gradle (Kotlin DSL) @@ -93,7 +93,7 @@ Currently, the framework supports the JVM, JS, WasmJS and iOS targets. ``` dependencies { - implementation("ai.koog:koog-agents:0.6.4") + implementation("ai.koog:koog-agents:0.7.1") } ``` 2. Make sure that you have `mavenCentral()` in the list of repositories. @@ -103,7 +103,7 @@ Currently, the framework supports the JVM, JS, WasmJS and iOS targets. ``` dependencies { - implementation 'ai.koog:koog-agents:0.6.4' + implementation 'ai.koog:koog-agents:0.7.1' } ``` 2. Make sure that you have `mavenCentral()` in the list of repositories. @@ -115,7 +115,7 @@ Currently, the framework supports the JVM, JS, WasmJS and iOS targets. ai.koog koog-agents-jvm - 0.6.4 + 0.7.1 ``` 2. Make sure that you have `mavenCentral` in the list of repositories. diff --git a/docs/docs/agents/graph-based-agents.md b/docs/docs/agents/graph-based-agents.md index 3ef8981a04..f585dc3837 100644 --- a/docs/docs/agents/graph-based-agents.md +++ b/docs/docs/agents/graph-based-agents.md @@ -77,26 +77,80 @@ which means the agent implementing this strategy will expect a string and return To create a strategy, use the [`strategy()`](https://api.koog.ai/agents/agents-core/ai.koog.agents.core.dsl.builder/strategy.html) function with two generics as the input and output types, provide a unique identifier for the strategy, and define the nodes and edges. - -```kotlin -val calculatorAgentStrategy = strategy("Simple calculator") { - val nodeSendInput by nodeLLMRequest() - val nodeExecuteTool by nodeExecuteTool() - val nodeSendToolResult by nodeLLMSendToolResult() - - edge(nodeStart forwardTo nodeSendInput) - edge(nodeSendInput forwardTo nodeFinish onAssistantMessage { true }) - edge(nodeSendInput forwardTo nodeExecuteTool onToolCall { true }) - edge(nodeExecuteTool forwardTo nodeSendToolResult) - edge(nodeSendToolResult forwardTo nodeFinish onAssistantMessage { true }) - edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCall { true }) -} -``` - +=== "Kotlin" + + + ```kotlin + val calculatorAgentStrategy = strategy("Simple calculator") { + val nodeSendInput by nodeLLMRequest() + val nodeExecuteTool by nodeExecuteTool() + val nodeSendToolResult by nodeLLMSendToolResult() + + edge(nodeStart forwardTo nodeSendInput) + edge(nodeSendInput forwardTo nodeFinish onAssistantMessage { true }) + edge(nodeSendInput forwardTo nodeExecuteTool onToolCall { true }) + edge(nodeExecuteTool forwardTo nodeSendToolResult) + edge(nodeSendToolResult forwardTo nodeFinish onAssistantMessage { true }) + edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCall { true }) + } + ``` + + +=== "Java" + + + + ```java + var calculatorAgentStrategy = AIAgentGraphStrategy.builder("Simple calculator") + .withInput(String.class) + .withOutput(String.class); + + var nodeSendInput = AIAgentNode.llmRequest(true, "nodeSendInput"); + var nodeExecuteTool = AIAgentNode.executeTool("nodeExecuteTool"); + var nodeSendToolResult = AIAgentNode.llmSendToolResult("nodeSendToolResult"); + + calculatorAgentStrategy.edge(calculatorAgentStrategy.nodeStart, nodeSendInput); + calculatorAgentStrategy.edge(AIAgentEdge.builder() + .from(nodeSendInput) + .to(calculatorAgentStrategy.nodeFinish) + .onCondition(msg -> msg instanceof Message.Assistant) + .transformed(msg -> ((Message.Assistant) msg).getContent()) + .build()); + calculatorAgentStrategy.edge(AIAgentEdge.builder() + .from(nodeSendInput) + .to(nodeExecuteTool) + .onCondition(msg -> msg instanceof Message.Tool.Call) + .transformed(msg -> (Message.Tool.Call) msg) + .build()); + calculatorAgentStrategy.edge(nodeExecuteTool, nodeSendToolResult); + calculatorAgentStrategy.edge(AIAgentEdge.builder() + .from(nodeSendToolResult) + .to(calculatorAgentStrategy.nodeFinish) + .onCondition(msg -> msg instanceof Message.Assistant) + .transformed(msg -> ((Message.Assistant) msg).getContent()) + .build()); + calculatorAgentStrategy.edge(AIAgentEdge.builder() + .from(nodeSendToolResult) + .to(nodeExecuteTool) + .onCondition(msg -> msg instanceof Message.Tool.Call) + .transformed(msg -> (Message.Tool.Call) msg) + .build()); + ``` + This example uses only [predefined nodes](../nodes-and-components.md), but you can also create [custom nodes](../custom-nodes.md). @@ -146,45 +200,114 @@ effectively transforming `Message.Assistant` to `String`, because `nodeFinish` e Let's create an agent instance with this strategy and run it: - - -```kotlin -val calculatorAgentStrategy = strategy("Simple calculator") { - val nodeSendInput by nodeLLMRequest() - val nodeExecuteTool by nodeExecuteTool() - val nodeSendToolResult by nodeLLMSendToolResult() - - edge(nodeStart forwardTo nodeSendInput) - edge(nodeSendInput forwardTo nodeFinish onAssistantMessage { true }) - edge(nodeSendInput forwardTo nodeExecuteTool onToolCall { true }) - edge(nodeExecuteTool forwardTo nodeSendToolResult) - edge(nodeSendToolResult forwardTo nodeFinish onAssistantMessage { true }) - edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCall { true }) -} - -val mathAgent = AIAgent( - promptExecutor = simpleOllamaAIExecutor(), - llmModel = OllamaModels.Meta.LLAMA_3_2, - strategy = calculatorAgentStrategy -) - -fun main() = runBlocking { - val result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.") - println(result) -} -``` - +=== "Kotlin" + + + ```kotlin + val calculatorAgentStrategy = strategy("Simple calculator") { + val nodeSendInput by nodeLLMRequest() + val nodeExecuteTool by nodeExecuteTool() + val nodeSendToolResult by nodeLLMSendToolResult() + + edge(nodeStart forwardTo nodeSendInput) + edge(nodeSendInput forwardTo nodeFinish onAssistantMessage { true }) + edge(nodeSendInput forwardTo nodeExecuteTool onToolCall { true }) + edge(nodeExecuteTool forwardTo nodeSendToolResult) + edge(nodeSendToolResult forwardTo nodeFinish onAssistantMessage { true }) + edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCall { true }) + } + + val mathAgent = AIAgent( + promptExecutor = simpleOllamaAIExecutor(), + llmModel = OllamaModels.Meta.LLAMA_3_2, + strategy = calculatorAgentStrategy + ) + + fun main() = runBlocking { + val result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.") + println(result) + } + ``` + + +=== "Java" + + + + ```java + var calculatorAgentStrategy = AIAgentGraphStrategy.builder("Simple calculator") + .withInput(String.class) + .withOutput(String.class); + + var nodeSendInput = AIAgentNode.llmRequest(true, "nodeSendInput"); + var nodeExecuteTool = AIAgentNode.executeTool("nodeExecuteTool"); + var nodeSendToolResult = AIAgentNode.llmSendToolResult("nodeSendToolResult"); + + calculatorAgentStrategy.edge(calculatorAgentStrategy.nodeStart, nodeSendInput); + calculatorAgentStrategy.edge(AIAgentEdge.builder() + .from(nodeSendInput) + .to(calculatorAgentStrategy.nodeFinish) + .onCondition(msg -> msg instanceof Message.Assistant) + .transformed(msg -> ((Message.Assistant) msg).getContent()) + .build()); + calculatorAgentStrategy.edge(AIAgentEdge.builder() + .from(nodeSendInput) + .to(nodeExecuteTool) + .onCondition(msg -> msg instanceof Message.Tool.Call) + .transformed(msg -> (Message.Tool.Call) msg) + .build()); + calculatorAgentStrategy.edge(nodeExecuteTool, nodeSendToolResult); + calculatorAgentStrategy.edge(AIAgentEdge.builder() + .from(nodeSendToolResult) + .to(calculatorAgentStrategy.nodeFinish) + .onCondition(msg -> msg instanceof Message.Assistant) + .transformed(msg -> ((Message.Assistant) msg).getContent()) + .build()); + calculatorAgentStrategy.edge(AIAgentEdge.builder() + .from(nodeSendToolResult) + .to(nodeExecuteTool) + .onCondition(msg -> msg instanceof Message.Tool.Call) + .transformed(msg -> (Message.Tool.Call) msg) + .build()); + + var promptExecutor = PromptExecutor.builder() + .ollama("http://localhost:11434") + .build(); + + AIAgent mathAgent = AIAgent.builder() + .promptExecutor(promptExecutor) + .llmModel(OllamaModels.Meta.LLAMA_3_2) + .graphStrategy(calculatorAgentStrategy.build()) + .build(); + + String result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.", null); + System.out.println(result); + ``` + When you run this agent, it will respond with something like this: @@ -235,106 +358,239 @@ Then the LLM will be able to decide to call tools that perform the calculations Define [tools](../tools-overview.md) for performing math operations and add them to a [ToolRegistry](https://api.koog.ai/agents/agents-tools/ai.koog.agents.core.tools/-tool-registry/index.html): - -```kotlin -@LLMDescription("Tools for performing math operations") -class MathTools : ToolSet { - @Tool - @LLMDescription("Adds two numbers and returns the result") - fun add(a: Int, b: Int): Int { - // This is not necessary, but it helps to see the tool call in the console output - println("Adding $a and $b...") - return a + b +=== "Kotlin" + + + ```kotlin + @LLMDescription("Tools for performing math operations") + class MathTools : ToolSet { + @Tool + @LLMDescription("Adds two numbers and returns the result") + fun add(a: Int, b: Int): Int { + // This is not necessary, but it helps to see the tool call in the console output + println("Adding $a and $b...") + return a + b + } + @Tool + @LLMDescription("Multiplies two numbers and returns the result") + fun multiply(a: Int, b: Int): Int { + // This is not necessary, but it helps to see the tool call in the console output + println("Multiplying $a and $b...") + return a * b + } } - @Tool - @LLMDescription("Multiplies two numbers and returns the result") - fun multiply(a: Int, b: Int): Int { - // This is not necessary, but it helps to see the tool call in the console output - println("Multiplying $a and $b...") - return a * b + + val toolRegistry = ToolRegistry { + tools(MathTools()) } -} + ``` + -val toolRegistry = ToolRegistry { - tools(MathTools()) -} -``` - +=== "Java" + + + + ```java + @LLMDescription("Tools for performing math operations") + public static class MathTools implements ToolSet { + @Tool + @LLMDescription("Adds two numbers and returns the result") + public int add(int a, int b) { + // This is not necessary, but it helps to see the tool call in the console output + System.out.println("Adding " + a + " and " + b + "..."); + return a + b; + } + + @Tool + @LLMDescription("Multiplies two numbers and returns the result") + public int multiply(int a, int b) { + // This is not necessary, but it helps to see the tool call in the console output + System.out.println("Multiplying " + a + " and " + b + "..."); + return a * b; + } + } + public static void main(String[] args) { + ToolRegistry toolRegistry = ToolRegistry.builder() + .tools(new MathTools()) + .build(); + } + ``` + Add the tool registry to the agent configuration: - -```kotlin -val mathAgent = AIAgent( - promptExecutor = simpleOllamaAIExecutor(), - llmModel = OllamaModels.Meta.LLAMA_3_2, - strategy = calculatorAgentStrategy, - toolRegistry = toolRegistry -) - -fun main() = runBlocking { - val result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.") - println(result) -} -``` - + + val calculatorAgentStrategy = strategy("Simple calculator") { + val nodeSendInput by nodeLLMRequest() + val nodeExecuteTool by nodeExecuteTool() + val nodeSendToolResult by nodeLLMSendToolResult() + + edge(nodeStart forwardTo nodeSendInput) + edge(nodeSendInput forwardTo nodeFinish onAssistantMessage { true }) + edge(nodeSendInput forwardTo nodeExecuteTool onToolCall { true }) + edge(nodeExecuteTool forwardTo nodeSendToolResult) + edge(nodeSendToolResult forwardTo nodeFinish onAssistantMessage { true }) + edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCall { true }) + } + --> + ```kotlin + val mathAgent = AIAgent( + promptExecutor = simpleOllamaAIExecutor(), + llmModel = OllamaModels.Meta.LLAMA_3_2, + strategy = calculatorAgentStrategy, + toolRegistry = toolRegistry + ) + + fun main() = runBlocking { + val result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.") + println(result) + } + ``` + + +=== "Java" + + + + ```java + AIAgent mathAgent = AIAgent.builder() + .promptExecutor(promptExecutor) + .llmModel(OllamaModels.Meta.LLAMA_3_2) + .graphStrategy(calculatorAgentStrategy.build()) + .toolRegistry(toolRegistry) + .build(); + + String result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.", null); + System.out.println(result); + ``` + When you run the agent now, it will respond with something like this: @@ -360,79 +616,171 @@ We can help the agent by describing its role and providing instructions for usin A [system prompt](../prompts/prompt-creation/index.md#system-message) defines the agent's role and instructions for performing tasks. In our example, it is important to describe how the agent should process complex multistep calculations: - -```kotlin -val mathAgent = AIAgent( - promptExecutor = simpleOllamaAIExecutor(), - llmModel = OllamaModels.Meta.LLAMA_3_2, - systemPrompt = """ - You are a simple calculator assistant. - You can add and multiply two numbers using the 'add' and 'multiply' tools. - When the user provides input, extract the numbers and operations they requested. - Use the appropriate tool for the first operation, then the next one, and so on, until you calculate the result. - Always respond with a clear, friendly message showing the calculation and result. - """.trimIndent(), - toolRegistry = toolRegistry, - strategy = calculatorAgentStrategy -) - -fun main() = runBlocking { - val result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.") - println(result) -} -``` - + --> + ```kotlin + val mathAgent = AIAgent( + promptExecutor = simpleOllamaAIExecutor(), + llmModel = OllamaModels.Meta.LLAMA_3_2, + systemPrompt = """ + You are a simple calculator assistant. + You can add and multiply two numbers using the 'add' and 'multiply' tools. + When the user provides input, extract the numbers and operations they requested. + Use the appropriate tool for the first operation, then the next one, and so on, until you calculate the result. + Always respond with a clear, friendly message showing the calculation and result. + """.trimIndent(), + toolRegistry = toolRegistry, + strategy = calculatorAgentStrategy + ) + + fun main() = runBlocking { + val result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.") + println(result) + } + ``` + + +=== "Java" + + + + ```java + AIAgent mathAgent = AIAgent.builder() + .promptExecutor(promptExecutor) + .llmModel(OllamaModels.Meta.LLAMA_3_2) + .systemPrompt("You are a simple calculator assistant. You can add and multiply two numbers using the 'add' and 'multiply' tools. When the user provides input, extract the numbers and operations they requested. Use the appropriate tool for the first operation, then the next one, and so on, until you calculate the result. Always respond with a clear, friendly message showing the calculation and result.") + .graphStrategy(calculatorAgentStrategy.build()) + .toolRegistry(toolRegistry) + .build(); + + String result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.", null); + System.out.println(result); + ``` + When you run the agent now, it will respond with something like this: diff --git a/docs/docs/agents/planner-agents/goap-agents.md b/docs/docs/agents/planner-agents/goap-agents.md index 40492ba5a8..f272f0ebd1 100644 --- a/docs/docs/agents/planner-agents/goap-agents.md +++ b/docs/docs/agents/planner-agents/goap-agents.md @@ -42,157 +42,349 @@ In the following example, GOAP handles high-level planning for creating an artic (outline → draft → review → publish), while the LLM performs the actual content generation within each action. - -```kotlin -// Define a state for content creation -data class ContentState( - val topic: String, - val hasOutline: Boolean = false, - val outline: String = "", - val hasDraft: Boolean = false, - val draft: String = "", - val hasReview: Boolean = false, - val isPublished: Boolean = false -): GoapAgentState(topic) { - override fun provideOutput(): String = draft -} - -// Create GOAP planner with LLM-powered actions -val planner = AIAgentPlannerStrategy.goap("content-planner", ::ContentState) { - // Define actions with preconditions and beliefs - action( - name = "Create outline", - precondition = { state -> !state.hasOutline }, - belief = { state -> state.copy(hasOutline = true, outline = "Outline") }, - cost = { 1.0 } - ) { ctx, state -> - // Use LLM to create the outline - val response = ctx.llm.writeSession { - appendPrompt { - user("Create a detailed outline for an article about: ${state.topic}") +=== "Kotlin" + + + ```kotlin + // Define a state for content creation + data class ContentState( + val topic: String, + val hasOutline: Boolean = false, + val outline: String = "", + val hasDraft: Boolean = false, + val draft: String = "", + val hasReview: Boolean = false, + val isPublished: Boolean = false + ): GoapAgentState(topic) { + override fun provideOutput(): String = draft + } + + // Create GOAP planner with LLM-powered actions + val planner = AIAgentPlannerStrategy.goap("content-planner", ::ContentState) { + // Define actions with preconditions and beliefs + action( + name = "Create outline", + precondition = { state -> !state.hasOutline }, + belief = { state -> state.copy(hasOutline = true, outline = "Outline") }, + cost = { 1.0 } + ) { ctx, state -> + // Use LLM to create the outline + val response = ctx.llm.writeSession { + appendPrompt { + user("Create a detailed outline for an article about: ${state.topic}") + } + requestLLM() } - requestLLM() + state.copy(hasOutline = true, outline = response.content) } - state.copy(hasOutline = true, outline = response.content) - } - action( - name = "Write draft", - precondition = { state -> state.hasOutline && !state.hasDraft }, - belief = { state -> state.copy(hasDraft = true, draft = "Draft") }, - cost = { 2.0 } - ) { ctx, state -> - // Use LLM to write the draft - val response = ctx.llm.writeSession { - appendPrompt { - user("Write an article based on this outline:\n${state.outline}") + action( + name = "Write draft", + precondition = { state -> state.hasOutline && !state.hasDraft }, + belief = { state -> state.copy(hasDraft = true, draft = "Draft") }, + cost = { 2.0 } + ) { ctx, state -> + // Use LLM to write the draft + val response = ctx.llm.writeSession { + appendPrompt { + user("Write an article based on this outline:\n${state.outline}") + } + requestLLM() } - requestLLM() + state.copy(hasDraft = true, draft = response.content) } - state.copy(hasDraft = true, draft = response.content) - } - action( - name = "Review content", - precondition = { state -> state.hasDraft && !state.hasReview }, - belief = { state -> state.copy(hasReview = true) }, - cost = { 1.0 } - ) { ctx, state -> - // Use LLM to review the draft - val response = ctx.llm.writeSession { - appendPrompt { - user("Review this article and suggest improvements:\n${state.draft}") + action( + name = "Review content", + precondition = { state -> state.hasDraft && !state.hasReview }, + belief = { state -> state.copy(hasReview = true) }, + cost = { 1.0 } + ) { ctx, state -> + // Use LLM to review the draft + val response = ctx.llm.writeSession { + appendPrompt { + user("Review this article and suggest improvements:\n${state.draft}") + } + requestLLM() } - requestLLM() + println("Review feedback: ${response.content}") + state.copy(hasReview = true) } - println("Review feedback: ${response.content}") - state.copy(hasReview = true) + + action( + name = "Publish", + precondition = { state -> state.hasReview && !state.isPublished }, + belief = { state -> state.copy(isPublished = true) }, + cost = { 1.0 } + ) { ctx, state -> + println("Publishing article...") + state.copy(isPublished = true) + } + + // Define the goal with a completion condition + goal( + name = "Published article", + description = "Complete and publish the article", + condition = { state -> state.isPublished } + ) } - action( - name = "Publish", - precondition = { state -> state.hasReview && !state.isPublished }, - belief = { state -> state.copy(isPublished = true) }, - cost = { 1.0 } - ) { ctx, state -> - println("Publishing article...") - state.copy(isPublished = true) + // Create and run the agent + val agentConfig = AIAgentConfig( + prompt = prompt("writer") { + system("You are a professional content writer.") + }, + model = OpenAIModels.Chat.GPT4o, + maxAgentIterations = 20 + ) + + val agent = AIAgent( + promptExecutor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")), + strategy = planner, + agentConfig = agentConfig + ) + + suspend fun main() { + val result = agent.run("The Future of AI in Software Development") + println("Final state: $result") + } + ``` + + +=== "Java" + + + + ```java + // Define a state for content creation + static class ContentState extends GoapAgentState { + public String topic; + public boolean hasOutline = false; + public String outline = ""; + public boolean hasDraft = false; + public String draft = ""; + public boolean hasReview = false; + public boolean isPublished = false; + + public ContentState(String topic) { + super(topic); + this.topic = topic; + } + + public ContentState copy(boolean hasOutline, String outline, boolean hasDraft, + String draft, boolean hasReview, boolean isPublished) { + ContentState state = new ContentState(topic); + state.hasOutline = hasOutline; + state.outline = outline; + state.hasDraft = hasDraft; + state.draft = draft; + state.hasReview = hasReview; + state.isPublished = isPublished; + return state; + } + + @Override + public String provideOutput() { + return draft; + } + } + + public static void main(String[] args) { + var promptExecutor = PromptExecutor.builder() + .openAI("OPENAI_API_KEY") + .build(); + + var strategy = AIAgentPlannerStrategy.builder("content-planner") + .goap(ContentState::new) + .action("Create outline", builder -> builder + .precondition(state -> !state.hasOutline) + .belief(state -> state.copy(true, "Outline", false, "", false, false)) + .cost(state -> 1.0) + .execute((context, state) -> { + String response = context.llm().writeSession(session -> { + session.appendPrompt(prompt -> { + prompt.user("Create a detailed outline for an article about: " + state.topic); + return null; + }); + return session.requestLLM().getContent(); + }); + return state.copy(true, response, state.hasDraft, state.draft, + state.hasReview, state.isPublished); + }) + ) + .action("Write draft", builder -> builder + .precondition(state -> state.hasOutline && !state.hasDraft) + .belief(state -> state.copy(state.hasOutline, state.outline, true, "Draft", false, false)) + .cost(state -> 2.0) + .execute((context, state) -> { + String response = context.llm().writeSession(session -> { + session.appendPrompt(prompt -> { + prompt.user("Write an article based on this outline:\n" + state.outline); + return null; + }); + return session.requestLLM().getContent(); + }); + return state.copy(state.hasOutline, state.outline, true, response, + state.hasReview, state.isPublished); + }) + ) + .action("Review content", builder -> builder + .precondition(state -> state.hasDraft && !state.hasReview) + .belief(state -> state.copy(state.hasOutline, state.outline, state.hasDraft, + state.draft, true, false)) + .cost(state -> 1.0) + .execute((context, state) -> { + String response = context.llm().writeSession(session -> { + session.appendPrompt(prompt -> { + prompt.user("Review this article and suggest improvements:\n" + state.draft); + return null; + }); + return session.requestLLM().getContent(); + }); + System.out.println("Review feedback: " + response); + return state.copy(state.hasOutline, state.outline, state.hasDraft, + state.draft, true, state.isPublished); + }) + ) + .action("Publish", builder -> builder + .precondition(state -> state.hasReview && !state.isPublished) + .belief(state -> state.copy(state.hasOutline, state.outline, state.hasDraft, + state.draft, state.hasReview, true)) + .cost(state -> 1.0) + .execute((context, state) -> { + System.out.println("Publishing article..."); + return state.copy(state.hasOutline, state.outline, state.hasDraft, + state.draft, state.hasReview, true); + }) + ) + .goal("Published article", builder -> builder + .description("Complete and publish the article") + .condition(state -> state.isPublished) + ) + .build(); + + var agent = AIAgent.builder() + .plannerStrategy(strategy) + .promptExecutor(promptExecutor) + .llmModel(OpenAIModels.Chat.GPT4o) + .systemPrompt("You are a professional content writer.") + .maxIterations(20) + .build(); + + String result = agent.run("The Future of AI in Software Development"); + System.out.println("Final state: " + result); + } + ``` + - // Define the goal with a completion condition - goal( - name = "Published article", - description = "Complete and publish the article", - condition = { state -> state.isPublished } - ) -} - -// Create and run the agent -val agentConfig = AIAgentConfig( - prompt = prompt("writer") { - system("You are a professional content writer.") - }, - model = OpenAIModels.Chat.GPT4o, - maxAgentIterations = 20 -) - -val agent = AIAgent( - promptExecutor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")), - strategy = planner, - agentConfig = agentConfig -) - -suspend fun main() { - val result = agent.run("The Future of AI in Software Development") - println("Final state: $result") -} -``` - ## Custom cost functions As [A* search] uses cost as a factor in finding the optimal sequence of actions, you can define custom cost functions for actions and goals to guide the planner: - -```kotlin -action( - name = "Expensive operation", - precondition = { true }, - belief = { state -> state.copy(operationDone = true) }, - cost = { state -> - // Dynamic cost based on state - if (state.hasOptimization) 1.0 else 10.0 +=== "Kotlin" + + + + ```kotlin + action( + name = "Expensive operation", + precondition = { true }, + belief = { state -> state.copy(operationDone = true) }, + cost = { state -> + // Dynamic cost based on state + if (state.hasOptimization) 1.0 else 10.0 + } + ) { ctx, state -> + // Execute action + state.copy(operationDone = true) + } + ``` + + +=== "Java" + + + + --> + ```java + .action("Expensive operation", builder -> builder + .precondition(state -> true) + .belief(state -> state.copy(true)) + .cost(state -> { + // Dynamic cost based on state + return state.hasOptimization ? 1.0 : 10.0; + }) + .execute((context, state) -> { + // Execute action + return state.copy(true); + }) + ) + ``` + ## State beliefs compared to actual execution @@ -203,42 +395,99 @@ GOAP distinguishes between the concepts of beliefs (optimistic predictions) and This allows the planner to make plans based on expected outcomes while handling actual results properly: - -```kotlin -action( - name = "Attempt complex task", - precondition = { state -> !state.taskComplete }, - belief = { state -> - // Optimistic belief: task will succeed - state.copy(taskComplete = true) - }, - cost = { 5.0 } -) { ctx, state -> - // Actual execution might fail or have different results - val success = performComplexTask() - state.copy( - taskComplete = success, - attempts = state.attempts + 1 +=== "Kotlin" + + + + ```kotlin + action( + name = "Attempt complex task", + precondition = { state -> !state.taskComplete }, + belief = { state -> + // Optimistic belief: task will succeed + state.copy(taskComplete = true) + }, + cost = { 5.0 } + ) { ctx, state -> + // Actual execution might fail or have different results + val success = performComplexTask() + state.copy( + taskComplete = success, + attempts = state.attempts + 1 + ) + } + ``` + + +=== "Java" + + + + ```java + .action("Attempt complex task", builder -> builder + .precondition(state -> !state.taskComplete) + .belief(state -> { + // Optimistic belief: task will succeed + return state.copy(true, state.attempts); + }) + .cost(state -> 5.0) + .execute((context, state) -> { + // Actual execution might fail or have different results + boolean success = performComplexTask(); + return state.copy(success, state.attempts + 1); + }) ) -} -``` - + ``` + [A* search]: https://en.wikipedia.org/wiki/A*_search_algorithm diff --git a/docs/docs/agents/planner-agents/llm-based-planners.md b/docs/docs/agents/planner-agents/llm-based-planners.md index 4f5679d2dd..fd2fc8274e 100644 --- a/docs/docs/agents/planner-agents/llm-based-planners.md +++ b/docs/docs/agents/planner-agents/llm-based-planners.md @@ -27,49 +27,92 @@ Koog provides two simple planners: The following example shows how to create a simple planner agent using `SimpleLLMPlanner`: - -```kotlin -// Create the planner -val planner = SimpleLLMPlanner() - -// Wrap it in a planner strategy -val strategy = AIAgentPlannerStrategy( - name = "simple-planner", - planner = planner -) - -// Configure the agent -val agentConfig = AIAgentConfig( - prompt = prompt("planner") { - system("You are a helpful planning assistant.") - }, - model = OpenAIModels.Chat.GPT4o, - maxAgentIterations = 50 -) - -// Create the planner agent -val agent = PlannerAIAgent( - promptExecutor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")), - strategy = strategy, - agentConfig = agentConfig -) - -suspend fun main() { +=== "Kotlin" + + + ```kotlin + // Create the planner + val planner = SimpleLLMPlanner() + + // Wrap it in a planner strategy + val strategy = AIAgentPlannerStrategy( + name = "simple-planner", + planner = planner + ) + + // Configure the agent + val agentConfig = AIAgentConfig( + prompt = prompt("planner") { + system("You are a helpful planning assistant.") + }, + model = OpenAIModels.Chat.GPT4o, + maxAgentIterations = 50 + ) + + // Create the planner agent + val agent = PlannerAIAgent( + promptExecutor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")), + strategy = strategy, + agentConfig = agentConfig + ) + + suspend fun main() { + // Run the agent with a task + val result = agent.run("Create a plan to organize a team meeting") + println(result) + } + ``` + + +=== "Java" + + + + ```java + // Create the planner strategy with LLM-based planner + AIAgentPlannerStrategy strategy = + AIAgentPlannerStrategy.builder("simple-planner") + .llmBasedPlanner() + .build(); + + // Create the OpenAI executor + var promptExecutor = PromptExecutor.builder() + .openAI("OPENAI_API_KEY") + .build(); + + // Create the planner agent using AIAgent builder + AIAgent agent = AIAgent.builder() + .plannerStrategy(strategy) + .promptExecutor(promptExecutor) + .llmModel(OpenAIModels.Chat.GPT4o) + .systemPrompt("You are a helpful planning assistant.") + .maxIterations(50) + .build(); + // Run the agent with a task - val result = agent.run("Create a plan to organize a team meeting") - println(result) -} -``` - + String result = agent.run("Create a plan to organize a team meeting"); + System.out.println(result); + ``` + ## Next steps diff --git a/docs/docs/history-compression.md b/docs/docs/history-compression.md index 71f0321b4f..764e5b66eb 100644 --- a/docs/docs/history-compression.md +++ b/docs/docs/history-compression.md @@ -81,14 +81,91 @@ Depending on which step you decide to perform compression, the following scenari === "Java" ```java + var graph = AIAgentGraphStrategy.builder("execute-with-history-compression") + .withInput(String.class) + .withOutput(String.class); + + var callLLM = AIAgentNode.llmRequest(); + var executeTool = AIAgentNode.executeTool(); + var sendToolResult = AIAgentNode.llmSendToolResult(); + + // Compress the LLM history and keep the current ReceivedToolResult for the next node + var compressHistory = AIAgentNode + .llmCompressHistory("compressHistory") + .withInput(ReceivedToolResult.class) + .build(); + + // Edge from start to callLLM + graph.edge(graph.nodeStart, callLLM); + + // Edge from callLLM to finish on assistant message + graph.edge(AIAgentEdge.builder() + .from(callLLM) + .to(graph.nodeFinish) + .onIsInstance(Message.Assistant.class) + .transformed(Message.Assistant::getContent) + .build()); + + // Edge from callLLM to executeTool on tool call + graph.edge(AIAgentEdge.builder() + .from(callLLM) + .to(executeTool) + .onIsInstance(Message.Tool.Call.class) + .build()); + + // Compress history after executing any tool if the history is too long + graph.edge(AIAgentEdge.builder() + .from(executeTool) + .to(compressHistory) + .onCondition((toolResult, ctx) -> + ctx.getLlm().readSession(session -> + session.getPrompt().getMessages().size() > 100 + ) + ) + .build()); + + graph.edge(compressHistory, sendToolResult); + + // Otherwise, proceed to the next LLM request + graph.edge(AIAgentEdge.builder() + .from(executeTool) + .to(sendToolResult) + .onCondition((toolResult, ctx) -> + ctx.getLlm().readSession(session -> + session.getPrompt().getMessages().size() <= 100 + ) + ) + .build()); + + // Edge from sendToolResult to executeTool on tool call + graph.edge(AIAgentEdge.builder() + .from(sendToolResult) + .to(executeTool) + .onIsInstance(Message.Tool.Call.class) + .build()); + + // Edge from sendToolResult to finish on assistant message + graph.edge(AIAgentEdge.builder() + .from(sendToolResult) + .to(graph.nodeFinish) + .onIsInstance(Message.Assistant.class) + .transformed(Message.Assistant::getContent) + .build()); ``` - + In this example, the strategy checks if the history is too long after each tool call. The history is compressed before sending the tool result back to the LLM. This prevents the context from growing during long conversations. @@ -121,14 +198,51 @@ The history is compressed before sending the tool result back to the LLM. This p === "Java" ```java - ``` - + var graph = AIAgentGraphStrategy.builder("execute-with-history-compression") + .withInput(String.class) + .withOutput(String.class); + + // Subgraph to collect information + var collectInformation = AIAgentSubgraph.builder("collectInformation") + .withInput(String.class) + .withOutput(String.class) + .limitedTools(Collections.emptyList()) + .withTask(input -> "Collect information based on: " + input) + .build(); + + // Compress history after collecting information + var compressHistory = AIAgentNode + .llmCompressHistory("compressHistory") + .withInput(String.class) + .build(); + + // Subgraph to make decision based on compressed history + var makeTheDecision = AIAgentSubgraph.builder("makeTheDecision") + .withInput(String.class) + .withOutput(String.class) + .limitedTools(Collections.emptyList()) + .withTask(input -> "Make a decision based on the information") + .build(); + + // Build the flow: start -> collectInformation -> compressHistory -> makeTheDecision -> finish + graph.edge(graph.nodeStart, collectInformation); + graph.edge(collectInformation, compressHistory); + graph.edge(compressHistory, makeTheDecision); + graph.edge(makeTheDecision, graph.nodeFinish); + ``` + In this example, the history is compressed after completing the information collection phase, but before proceeding to the decision-making phase. @@ -166,12 +280,8 @@ If you are implementing a custom node, you can compress history using the `repla **/ --> ```java - // FAILED: `replaceHistoryWithTLDR()` is a suspend Kotlin extension on AIAgentLLMWriteSession - // and requires a coroutine context. From Java, calling suspend functions directly - // would require passing a Continuation and coroutine machinery, which is not part of the - // public Java API. No non-suspending Java wrapper is available for this action. ``` - + This approach gives you more flexibility to implement compression at any point in your custom node logic, based on your specific requirements. @@ -217,18 +327,32 @@ You can use it as follows: === "Java" ```java - // FAILED: `nodeLLMCompressHistory(strategy = HistoryCompressionStrategy.WholeHistory)` - // is a Kotlin DSL node with a reified generic. There is no Java builder to add this node - // into a graph strategy. The HistoryCompressionStrategy type exists, but invoking the DSL - // from Java is not supported. + // Using WholeHistory strategy in a compression node + var compressHistory = AIAgentNode + .llmCompressHistory("compressHistory") + .withInput(String.class) + .compressionStrategy(HistoryCompressionStrategy.WholeHistory) + .build(); + + // Note: This example only shows the node creation. + // You would need to add edges and other nodes to complete the graph. ``` - + * In a custom node: @@ -263,11 +387,8 @@ You can use it as follows: **/ --> ```java - // FAILED: `replaceHistoryWithTLDR(strategy = HistoryCompressionStrategy.WholeHistory)` is a suspend - // Kotlin extension on AIAgentLLMWriteSession and requires coroutines. No non-suspending Java - // wrapper exists in the current Koog API to perform this operation. ``` - + ### FromLastNMessages @@ -304,17 +425,32 @@ You can use it as follows: === "Java" ```java - // FAILED: The Kotlin DSL node `nodeLLMCompressHistory( - // strategy = HistoryCompressionStrategy.FromLastNMessages(5) - // )` relies on reified generics and DSL builders not callable from Java. + // Using FromLastNMessages strategy to compress only the last 5 messages + var compressHistory = AIAgentNode + .llmCompressHistory("compressHistory") + .withInput(String.class) + .compressionStrategy(HistoryCompressionStrategy.FromLastNMessages(5)) + .build(); + + // Note: This example only shows the node creation. + // You would need to add edges and other nodes to complete the graph. ``` - + * In a custom node: @@ -350,10 +486,8 @@ You can use it as follows: **/ --> ```java - // FAILED: Calling `replaceHistoryWithTLDR(strategy = HistoryCompressionStrategy.FromLastNMessages(5))` - // requires a suspend Kotlin context. There is no Java-accessible non-suspending API for this. ``` - + ### Chunked @@ -390,17 +524,32 @@ You can use it as follows: === "Java" ```java - // FAILED: The Kotlin graph DSL and `nodeLLMCompressHistory( - // strategy = HistoryCompressionStrategy.Chunked(10) - // )` are not available from Java due to reified generics and lack of a Java graph builder API. + // Using Chunked strategy to compress history in chunks of 10 messages + var compressHistory = AIAgentNode + .llmCompressHistory("compressHistory") + .withInput(String.class) + .compressionStrategy(HistoryCompressionStrategy.Chunked(10)) + .build(); + + // Note: This example only shows the node creation. + // You would need to add edges and other nodes to complete the graph. ``` - + * In a custom node: @@ -436,10 +585,8 @@ You can use it as follows: **/ --> ```java - // FAILED: `replaceHistoryWithTLDR(strategy = HistoryCompressionStrategy.Chunked(10))` is a suspend - // Kotlin API; there is no current non-suspending Java entry point to perform this action inside a node. ``` - + ### RetrieveFactsFromHistory @@ -499,19 +646,52 @@ You can use it as follows: === "Java" - + ```java - // FAILED: `nodeLLMCompressHistory(strategy = new RetrieveFactsFromHistory(...))` - // is part of the Kotlin graph DSL with reified generics; Java cannot call it and there is no - // equivalent Java graph builder API to inject this node. + // Using RetrieveFactsFromHistory strategy to extract specific facts + var compressHistory = AIAgentNode + .llmCompressHistory("compressHistory") + .withInput(ReceivedToolResult.class) + .compressionStrategy(new RetrieveFactsFromHistory( + new Concept( + "user_preferences", + "User's preferences for the recommendation system, including the preferred conversation style, theme in the application, etc.", + FactType.MULTIPLE + ), + new Concept( + "product_details", + "Brief details about products in the catalog the user has been checking", + FactType.MULTIPLE + ), + new Concept( + "issue_solved", + "Was the initial user's issue resolved?", + FactType.SINGLE + ) + )) + .build(); + + // Note: This example only shows the node creation. + // You would need to add edges and other nodes to complete the graph. ``` - + * In a custom node: @@ -573,10 +753,8 @@ You can use it as follows: **/ --> ```java - // FAILED: `replaceHistoryWithTLDR(strategy = new RetrieveFactsFromHistory(...))` is a suspend - // Kotlin extension requiring coroutines; no Java wrapper is available. ``` - + ## Custom history compression strategy implementation @@ -633,11 +811,8 @@ Here is an example: **/ --> ```java - // FAILED: Custom strategies must extend Kotlin `HistoryCompressionStrategy` and override the - // suspend method `compress(AIAgentLLMWriteSession, List)`. Java cannot implement - // suspend functions directly. A Kotlin shim would be required; no direct Java implementation is possible. ``` - + In this example, the custom strategy filters messages that contain the word "important" and keeps only those in the compressed history. @@ -675,11 +850,8 @@ Then you can use it as follows: **/ --> ```java - // FAILED: Even with a custom strategy, adding it via `nodeLLMCompressHistory( - // strategy = new MyCustomCompressionStrategy() - // )` is Kotlin DSL-only with reified generics; no Java graph builder is provided. ``` - + * In a custom node: @@ -715,10 +887,8 @@ Then you can use it as follows: **/ --> ```java - // FAILED: `replaceHistoryWithTLDR(strategy = new MyCustomCompressionStrategy())` is a suspend - // Kotlin API; not callable from Java without coroutine interop glue, which the public API lacks. ``` - + ## Memory preservation during compression @@ -754,16 +924,33 @@ You can use the `preserveMemory` parameter as follows: === "Java" ```java - // FAILED: Passing `preserveMemory = true` to `nodeLLMCompressHistory(...)` - // still relies on the Kotlin graph DSL with reified generics; Java cannot use this API directly. + // Using WholeHistory strategy with preserveMemory=true + var compressHistory = AIAgentNode + .llmCompressHistory("compressHistory") + .withInput(String.class) + .compressionStrategy(HistoryCompressionStrategy.WholeHistory) + .preserveMemory(true) + .build(); + + // Note: This example only shows the node creation. + // You would need to add edges and other nodes to complete the graph. ``` - + * In a custom node: @@ -802,7 +989,5 @@ You can use the `preserveMemory` parameter as follows: **/ --> ```java - // FAILED: `replaceHistoryWithTLDR(strategy = HistoryCompressionStrategy.WholeHistory, preserveMemory = true)` - // is a suspend Kotlin extension; there is no Java-accessible non-suspending wrapper in Koog. ``` - + diff --git a/docs/docs/nodes-and-components.md b/docs/docs/nodes-and-components.md index c2f7229976..e0565cfbc9 100644 --- a/docs/docs/nodes-and-components.md +++ b/docs/docs/nodes-and-components.md @@ -27,21 +27,43 @@ graph LR Here is how you can define a node that expects a string as input and returns the length of the string (an integer) as output: - - -```kotlin -val nodeLength by node { input -> - input.length -} -``` - +=== "Kotlin" + + + + ```kotlin + val nodeLength by node { input -> + input.length + } + ``` + + +=== "Java" + + + + ```java + var nodeLength = AIAgentNode.builder("nodeLength") + .withInput(String.class) + .withOutput(Integer.class) + .withAction((input, ctx) -> input.length()) + .build(); + ``` + For more information, see [`node()`](api:agents-core::ai.koog.agents.core.dsl.builder.AIAgentSubgraphBuilderBase.node). @@ -73,24 +95,52 @@ You can use this node for the following purposes: Here is an example: - - -```kotlin -val passthrough by nodeDoNothing("passthrough") - -edge(nodeStart forwardTo passthrough) -edge(passthrough forwardTo nodeFinish) -``` - +=== "Kotlin" + + + + ```kotlin + val passthrough by nodeDoNothing("passthrough") + + edge(nodeStart forwardTo passthrough) + edge(passthrough forwardTo nodeFinish) + ``` + + +=== "Java" + + + + ```java + var passthrough = AIAgentNode.builder("passthrough") + .withInput(String.class) + .withOutput(String.class) + .withAction((input, ctx) -> input) + .build(); + + strategy.edge(strategy.nodeStart, passthrough); + strategy.edge(passthrough, strategy.nodeFinish); + ``` + ## LLM nodes @@ -122,39 +172,94 @@ You can use this node for the following purposes: Here is an example: - - -```kotlin -val firstNode by node { - // Transform input to output -} - -val secondNode by node { - // Transform output to output -} - -// Node will get the value of type Output as input from the previous node and path through it to the next node -val setupContext by nodeAppendPrompt("setupContext") { - system("You are a helpful assistant specialized in Kotlin programming.") - user("I need help with Kotlin coroutines.") -} - -edge(firstNode forwardTo setupContext) -edge(setupContext forwardTo secondNode) -``` - +=== "Kotlin" + + + + ```kotlin + val firstNode by node { + // Transform input to output + } + + val secondNode by node { + // Transform output to output + } + + // Node will get the value of type Output as input from the previous node and path through it to the next node + val setupContext by nodeAppendPrompt("setupContext") { + system("You are a helpful assistant specialized in Kotlin programming.") + user("I need help with Kotlin coroutines.") + } + + edge(firstNode forwardTo setupContext) + edge(setupContext forwardTo secondNode) + ``` + + +=== "Java" + + + + ```java + var firstNode = AIAgentNode.builder("firstNode") + .withInput(Object.class) + .withOutput(Object.class) + .withAction((input, ctx) -> { + // Transform input to output + return input; + }) + .build(); + + var secondNode = AIAgentNode.builder("secondNode") + .withInput(Object.class) + .withOutput(Object.class) + .withAction((output, ctx) -> { + // Transform output to output + return output; + }) + .build(); + + var setupContext = AIAgentNode.builder("setupContext") + .withInput(Object.class) + .withOutput(Object.class) + .withAction((output, ctx) -> { + ctx.getLlm().writeSession(session -> { + session.appendPrompt(prompt -> { + prompt.system("You are a helpful assistant specialized in Kotlin programming."); + prompt.user("I need help with Kotlin coroutines."); + return null; + }); + return null; + }); + return output; + }) + .build(); + + strategy.edge(firstNode, setupContext); + strategy.edge(setupContext, secondNode); + ``` + ### nodeLLMSendMessageOnlyCallingTools @@ -220,24 +325,53 @@ You can use this node for the following purposes: Here is an example: - - -```kotlin -val requestLLM by nodeLLMRequest("requestLLM", allowToolCalls = true) -edge(getUserQuestion forwardTo requestLLM) -``` - +=== "Kotlin" + + + + ```kotlin + val requestLLM by nodeLLMRequest("requestLLM", allowToolCalls = true) + edge(getUserQuestion forwardTo requestLLM) + ``` + + +=== "Java" + + + + ```java + var getUserQuestion = AIAgentNode.builder("getUserQuestion") + .withInput(String.class) + .withOutput(String.class) + .withAction((input, ctx) -> input) + .build(); + + var requestLLM = AIAgentNode.llmRequest(true, "requestLLM"); + + strategy.edge(getUserQuestion, requestLLM); + ``` + ### nodeLLMRequestStructured @@ -304,24 +438,53 @@ You can use this node for the following purposes: Here is an example: - - -```kotlin -val requestLLMMultipleTools by nodeLLMRequestMultiple() -edge(getComplexUserQuestion forwardTo requestLLMMultipleTools) -``` - +=== "Kotlin" + + + + ```kotlin + val requestLLMMultipleTools by nodeLLMRequestMultiple() + edge(getComplexUserQuestion forwardTo requestLLMMultipleTools) + ``` + + +=== "Java" + + + + ```java + var getComplexUserQuestion = AIAgentNode.builder("getComplexUserQuestion") + .withInput(String.class) + .withOutput(String.class) + .withAction((input, ctx) -> input) + .build(); + + var requestLLMMultipleTools = AIAgentNode.llmRequestMultiple("requestLLMMultipleTools"); + + strategy.edge(getComplexUserQuestion, requestLLMMultipleTools); + ``` + ### nodeLLMCompressHistory @@ -353,29 +516,60 @@ You can use this node for the following purposes: Here is an example: - - -```kotlin -val compressHistory by nodeLLMCompressHistory( - "compressHistory", - strategy = HistoryCompressionStrategy.FromLastNMessages(10), - preserveMemory = true -) -edge(generateHugeHistory forwardTo compressHistory) -``` - +=== "Kotlin" + + + + ```kotlin + val compressHistory by nodeLLMCompressHistory( + "compressHistory", + strategy = HistoryCompressionStrategy.FromLastNMessages(10), + preserveMemory = true + ) + edge(generateHugeHistory forwardTo compressHistory) + ``` + + +=== "Java" + + + + ```java + var generateHugeHistory = AIAgentNode.builder("generateHugeHistory") + .withInput(String.class) + .withOutput(String.class) + .withAction((input, ctx) -> input) + .build(); + + var compressHistory = AIAgentNode.llmCompressHistory("compressHistory") + .withInput(String.class) + .build(); + + strategy.edge(generateHugeHistory, compressHistory); + ``` + ## Tool nodes @@ -406,25 +600,56 @@ You can use this node for the following purposes: Here is an example: - - -```kotlin -val requestLLM by nodeLLMRequest() -val executeTool by nodeExecuteTool() -edge(requestLLM forwardTo executeTool onToolCall { true }) -``` - +=== "Kotlin" + + + + ```kotlin + val requestLLM by nodeLLMRequest() + val executeTool by nodeExecuteTool() + edge(requestLLM forwardTo executeTool onToolCall { true }) + ``` + + +=== "Java" + + + + ```java + var requestLLM = AIAgentNode.llmRequest(true, "requestLLM"); + var executeTool = AIAgentNode.executeTool("executeTool"); + + strategy.edge(AIAgentEdge.builder() + .from(requestLLM) + .to(executeTool) + .onCondition(msg -> msg instanceof Message.Tool.Call) + .transformed(msg -> (Message.Tool.Call) msg) + .build()); + ``` + ### nodeLLMSendToolResult @@ -453,24 +678,48 @@ You can use this node for the following purposes: Here is an example: - - -```kotlin -val executeTool by nodeExecuteTool() -val sendToolResultToLLM by nodeLLMSendToolResult() -edge(executeTool forwardTo sendToolResultToLLM) -``` - +=== "Kotlin" + + + + ```kotlin + val executeTool by nodeExecuteTool() + val sendToolResultToLLM by nodeLLMSendToolResult() + edge(executeTool forwardTo sendToolResultToLLM) + ``` + + +=== "Java" + + + + ```java + var executeTool = AIAgentNode.executeTool("executeTool"); + var sendToolResultToLLM = AIAgentNode.llmSendToolResult("sendToolResultToLLM"); + + strategy.edge(executeTool, sendToolResultToLLM); + ``` + ### nodeExecuteMultipleTools @@ -499,25 +748,61 @@ You can use this node for the following purposes: Here is an example: - - -```kotlin -val requestLLMMultipleTools by nodeLLMRequestMultiple() -val executeMultipleTools by nodeExecuteMultipleTools() -edge(requestLLMMultipleTools forwardTo executeMultipleTools onMultipleToolCalls { true }) -``` - +=== "Kotlin" + + + + ```kotlin + val requestLLMMultipleTools by nodeLLMRequestMultiple() + val executeMultipleTools by nodeExecuteMultipleTools() + edge(requestLLMMultipleTools forwardTo executeMultipleTools onMultipleToolCalls { true }) + ``` + + +=== "Java" + + + + ```java + var requestLLMMultipleTools = AIAgentNode.llmRequestMultiple("requestLLMMultipleTools"); + var executeMultipleTools = AIAgentNode.executeMultipleTools(false, "executeMultipleTools"); + + // Extract tool calls from the list of responses + strategy.edge(AIAgentEdge.builder() + .from(requestLLMMultipleTools) + .to(executeMultipleTools) + .onCondition(responses -> responses.stream() + .anyMatch(msg -> msg instanceof Message.Tool.Call)) + .transformed(responses -> responses.stream() + .filter(msg -> msg instanceof Message.Tool.Call) + .map(msg -> (Message.Tool.Call) msg) + .toList()) + .build()); + ``` + ### nodeLLMSendMultipleToolResults @@ -546,24 +831,49 @@ You can use this node for the following purposes: Here is an example: - - -```kotlin -val executeMultipleTools by nodeExecuteMultipleTools() -val sendMultipleToolResultsToLLM by nodeLLMSendMultipleToolResults() -edge(executeMultipleTools forwardTo sendMultipleToolResultsToLLM) -``` - +=== "Kotlin" + + + + ```kotlin + val executeMultipleTools by nodeExecuteMultipleTools() + val sendMultipleToolResultsToLLM by nodeLLMSendMultipleToolResults() + edge(executeMultipleTools forwardTo sendMultipleToolResultsToLLM) + ``` + + +=== "Java" + + + + ```java + var executeMultipleTools = AIAgentNode.executeMultipleTools(false, "executeMultipleTools"); + var sendMultipleToolResultsToLLM = AIAgentNode.llmSendMultipleToolResults("sendMultipleToolResultsToLLM"); + + strategy.edge(executeMultipleTools, sendMultipleToolResultsToLLM); + ``` + ## Node output transformation @@ -593,68 +903,152 @@ graph LR The [`transform()`](api:agents-core::ai.koog.agents.core.dsl.builder.AIAgentNodeDelegate.transform) function creates a new `AIAgentNodeDelegate` that wraps the original node and applies a transformation function to its output. - - -```kotlin -inline fun AIAgentNodeDelegate.transform( - noinline transformation: suspend (Output) -> T -): AIAgentNodeDelegate -``` - +=== "Kotlin" + + + + ```kotlin + inline fun AIAgentNodeDelegate.transform( + noinline transformation: suspend (Output) -> T + ): AIAgentNodeDelegate + ``` + + +=== "Java" + + ```java + // In Java, you need to manually compose nodes + // with transformation logic using AIAgentNode.builder() and explicit type parameters. + // See the examples below for the Java approach to node transformations. + ``` + #### Custom node transformation Transform the output of a custom node to a different data type: - - -```kotlin -val textNode by nodeDoNothing("textNode").transform { text -> - text.split(" ").filter { it.isNotBlank() }.size -} - -edge(nodeStart forwardTo textNode) -edge(textNode forwardTo nodeFinish) -``` - +=== "Kotlin" + + + + ```kotlin + val textNode by nodeDoNothing("textNode").transform { text -> + text.split(" ").filter { it.isNotBlank() }.size + } + + edge(nodeStart forwardTo textNode) + edge(textNode forwardTo nodeFinish) + ``` + + +=== "Java" + + + + ```java + var textNode = AIAgentNode.builder("textNode") + .withInput(String.class) + .withOutput(Integer.class) + .withAction((text, ctx) -> { + String[] words = text.split(" "); + int count = 0; + for (String word : words) { + if (!word.isBlank()) { + count++; + } + } + return count; + }) + .build(); + + strategy.edge(strategy.nodeStart, textNode); + strategy.edge(textNode, strategy.nodeFinish); + ``` + #### Built-in node transformation Transform the output of built-in nodes like `nodeLLMRequest`: - - -```kotlin -val lengthNode by nodeLLMRequest("llmRequest").transform { assistantMessage -> - assistantMessage.content.length -} - -edge(nodeStart forwardTo lengthNode) -edge(lengthNode forwardTo nodeFinish) -``` - +=== "Kotlin" + + + + ```kotlin + val lengthNode by nodeLLMRequest("llmRequest").transform { assistantMessage -> + assistantMessage.content.length + } + + edge(nodeStart forwardTo lengthNode) + edge(lengthNode forwardTo nodeFinish) + ``` + + +=== "Java" + + + + ```java + var llmRequest = AIAgentNode.llmRequest(true, "llmRequest"); + var lengthNode = AIAgentNode.builder("lengthNode") + .withInput(Message.Response.class) + .withOutput(Integer.class) + .withAction((assistantMessage, ctx) -> { + if (assistantMessage instanceof Message.Assistant) { + return ((Message.Assistant) assistantMessage).getContent().length(); + } + return 0; + }) + .build(); + + strategy.edge(strategy.nodeStart, llmRequest); + strategy.edge(llmRequest, lengthNode); + strategy.edge(lengthNode, strategy.nodeFinish); + ``` + ## Predefined subgraphs @@ -687,38 +1081,71 @@ The API allows you to fine‑tune execution with optional parameters: You can provide a task to the subgraph as text, configure the LLM if needed, and provide the necessary tools, and the subgraph will process and solve the task. Here is an example: - - -```kotlin -val processQuery by subgraphWithTask( - tools = listOf(searchTool, calculatorTool, weatherTool), - llmModel = OpenAIModels.Chat.GPT4o, - runMode = ToolCalls.SEQUENTIAL, - assistantResponseRepeatMax = 3, -) { userQuery -> - """ - You are a helpful assistant that can answer questions about various topics. - Please help with the following query: - $userQuery - """ -} -``` - +=== "Kotlin" + + + + ```kotlin + val processQuery by subgraphWithTask( + tools = listOf(searchTool, calculatorTool, weatherTool), + llmModel = OpenAIModels.Chat.GPT4o, + runMode = ToolCalls.SEQUENTIAL, + assistantResponseRepeatMax = 3, + ) { userQuery -> + """ + You are a helpful assistant that can answer questions about various topics. + Please help with the following query: + $userQuery + """ + } + ``` + + +=== "Java" + + + + ```java + var processQuery = AIAgentSubgraph.builder("processQuery") + .limitedTools(List.of(searchTool, calculatorTool, weatherTool)) + .withInput(String.class) + .withOutput(String.class) + .withTask(userQuery -> + "You are a helpful assistant that can answer questions about various topics.\n" + + "Please help with the following query:\n" + + userQuery) + .build(); + ``` + ### subgraphWithVerification @@ -734,42 +1161,77 @@ You can use this subgraph for the following purposes: The subgraph ensures that the LLM calls a verification tool at the end of the workflow to check whether the task was successfully completed. It guarantees this verification is performed as the final step and returns a `CriticResult` that indicates whether a task was completed successfully and provides detailed feedback. Here is an example: - - -```kotlin -val verifyCode by subgraphWithVerification( - tools = listOf(runTestsTool, analyzeTool, readFileTool), - llmModel = AnthropicModels.Opus_4_6, - runMode = ToolCalls.SEQUENTIAL, - assistantResponseRepeatMax = 3, -) { codeToVerify -> - """ - You are a code reviewer. Please verify that the following code meets all requirements: - 1. It compiles without errors - 2. All tests pass - 3. It follows the project's coding standards - - Code to verify: - $codeToVerify - """ -} -``` - +=== "Kotlin" + + + + ```kotlin + val verifyCode by subgraphWithVerification( + tools = listOf(runTestsTool, analyzeTool, readFileTool), + llmModel = AnthropicModels.Opus_4_6, + runMode = ToolCalls.SEQUENTIAL, + assistantResponseRepeatMax = 3, + ) { codeToVerify -> + """ + You are a code reviewer. Please verify that the following code meets all requirements: + 1. It compiles without errors + 2. All tests pass + 3. It follows the project's coding standards + + Code to verify: + $codeToVerify + """ + } + ``` + + +=== "Java" + + + + ```java + var verifyCode = AIAgentSubgraph.builder("verifyCode") + .limitedTools(List.of(runTestsTool, analyzeTool, readFileTool)) + .withInput(String.class) + .withVerification(codeToVerify -> + "You are a code reviewer. Please verify that the following code meets all requirements:\n" + + "1. It compiles without errors\n" + + "2. All tests pass\n" + + "3. It follows the project's coding standards\n\n" + + "Code to verify:\n" + + codeToVerify) + .build(); + ``` + ## Predefined strategies and common strategy patterns @@ -785,85 +1247,210 @@ returns a result. You can use this strategy when you need to run straightforward processes that do not require complex logic. - -```kotlin - -public fun singleRunStrategy(): AIAgentGraphStrategy = strategy("single_run") { - val nodeCallLLM by nodeLLMRequest("sendInput") - val nodeExecuteTool by nodeExecuteTool("nodeExecuteTool") - val nodeSendToolResult by nodeLLMSendToolResult("nodeSendToolResult") - - edge(nodeStart forwardTo nodeCallLLM) - edge(nodeCallLLM forwardTo nodeExecuteTool onToolCall { true }) - edge(nodeCallLLM forwardTo nodeFinish onAssistantMessage { true }) - edge(nodeExecuteTool forwardTo nodeSendToolResult) - edge(nodeSendToolResult forwardTo nodeFinish onAssistantMessage { true }) - edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCall { true }) -} -``` - +=== "Kotlin" + + + ```kotlin + public fun singleRunStrategy(): AIAgentGraphStrategy = strategy("single_run") { + val nodeCallLLM by nodeLLMRequest("sendInput") + val nodeExecuteTool by nodeExecuteTool("nodeExecuteTool") + val nodeSendToolResult by nodeLLMSendToolResult("nodeSendToolResult") + + edge(nodeStart forwardTo nodeCallLLM) + edge(nodeCallLLM forwardTo nodeExecuteTool onToolCall { true }) + edge(nodeCallLLM forwardTo nodeFinish onAssistantMessage { true }) + edge(nodeExecuteTool forwardTo nodeSendToolResult) + edge(nodeSendToolResult forwardTo nodeFinish onAssistantMessage { true }) + edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCall { true }) + } + ``` + + +=== "Java" + + + + ```java + public static AIAgentGraphStrategy singleRunStrategy() { + var strategy = AIAgentGraphStrategy.builder("single_run") + .withInput(String.class) + .withOutput(String.class); + + var nodeCallLLM = AIAgentNode.llmRequest(true, "sendInput"); + var nodeExecuteTool = AIAgentNode.executeTool("nodeExecuteTool"); + var nodeSendToolResult = AIAgentNode.llmSendToolResult("nodeSendToolResult"); + + strategy.edge(strategy.nodeStart, nodeCallLLM); + strategy.edge(AIAgentEdge.builder() + .from(nodeCallLLM) + .to(nodeExecuteTool) + .onCondition(msg -> msg instanceof Message.Tool.Call) + .transformed(msg -> (Message.Tool.Call) msg) + .build()); + strategy.edge(AIAgentEdge.builder() + .from(nodeCallLLM) + .to(strategy.nodeFinish) + .onCondition(msg -> msg instanceof Message.Assistant) + .transformed(msg -> ((Message.Assistant) msg).getContent()) + .build()); + strategy.edge(nodeExecuteTool, nodeSendToolResult); + strategy.edge(AIAgentEdge.builder() + .from(nodeSendToolResult) + .to(strategy.nodeFinish) + .onCondition(msg -> msg instanceof Message.Assistant) + .transformed(msg -> ((Message.Assistant) msg).getContent()) + .build()); + strategy.edge(AIAgentEdge.builder() + .from(nodeSendToolResult) + .to(nodeExecuteTool) + .onCondition(msg -> msg instanceof Message.Tool.Call) + .transformed(msg -> (Message.Tool.Call) msg) + .build()); + + return strategy.build(); + } + ``` + ### Tool-based strategy A tool-based strategy is designed for workflows that heavily rely on tools to perform specific operations. It typically executes tools based on the LLM decisions and processes the results. - -```kotlin -fun toolBasedStrategy(name: String, toolRegistry: ToolRegistry): AIAgentGraphStrategy { - return strategy(name) { - val nodeSendInput by nodeLLMRequest() - val nodeExecuteTool by nodeExecuteTool() - val nodeSendToolResult by nodeLLMSendToolResult() +=== "Kotlin" + + + ```kotlin + fun toolBasedStrategy(name: String, toolRegistry: ToolRegistry): AIAgentGraphStrategy { + return strategy(name) { + val nodeSendInput by nodeLLMRequest() + val nodeExecuteTool by nodeExecuteTool() + val nodeSendToolResult by nodeLLMSendToolResult() + + // Define the flow of the agent + edge(nodeStart forwardTo nodeSendInput) + + // If the LLM responds with a message, finish + edge( + (nodeSendInput forwardTo nodeFinish) + onAssistantMessage { true } + ) + + // If the LLM calls a tool, execute it + edge( + (nodeSendInput forwardTo nodeExecuteTool) + onToolCall { true } + ) + + // Send the tool result back to the LLM + edge(nodeExecuteTool forwardTo nodeSendToolResult) + + // If the LLM calls another tool, execute it + edge( + (nodeSendToolResult forwardTo nodeExecuteTool) + onToolCall { true } + ) + + // If the LLM responds with a message, finish + edge( + (nodeSendToolResult forwardTo nodeFinish) + onAssistantMessage { true } + ) + } + } + ``` + + +=== "Java" + + + + ```java + public static AIAgentGraphStrategy toolBasedStrategy(String name, ToolRegistry toolRegistry) { + var strategy = AIAgentGraphStrategy.builder(name) + .withInput(String.class) + .withOutput(String.class); + + var nodeSendInput = AIAgentNode.llmRequest(true, "nodeSendInput"); + var nodeExecuteTool = AIAgentNode.executeTool("nodeExecuteTool"); + var nodeSendToolResult = AIAgentNode.llmSendToolResult("nodeSendToolResult"); // Define the flow of the agent - edge(nodeStart forwardTo nodeSendInput) + strategy.edge(strategy.nodeStart, nodeSendInput); // If the LLM responds with a message, finish - edge( - (nodeSendInput forwardTo nodeFinish) - onAssistantMessage { true } - ) + strategy.edge(AIAgentEdge.builder() + .from(nodeSendInput) + .to(strategy.nodeFinish) + .onCondition(msg -> msg instanceof Message.Assistant) + .transformed(msg -> ((Message.Assistant) msg).getContent()) + .build()); // If the LLM calls a tool, execute it - edge( - (nodeSendInput forwardTo nodeExecuteTool) - onToolCall { true } - ) + strategy.edge(AIAgentEdge.builder() + .from(nodeSendInput) + .to(nodeExecuteTool) + .onCondition(msg -> msg instanceof Message.Tool.Call) + .transformed(msg -> (Message.Tool.Call) msg) + .build()); // Send the tool result back to the LLM - edge(nodeExecuteTool forwardTo nodeSendToolResult) + strategy.edge(nodeExecuteTool, nodeSendToolResult); // If the LLM calls another tool, execute it - edge( - (nodeSendToolResult forwardTo nodeExecuteTool) - onToolCall { true } - ) + strategy.edge(AIAgentEdge.builder() + .from(nodeSendToolResult) + .to(nodeExecuteTool) + .onCondition(msg -> msg instanceof Message.Tool.Call) + .transformed(msg -> (Message.Tool.Call) msg) + .build()); // If the LLM responds with a message, finish - edge( - (nodeSendToolResult forwardTo nodeFinish) - onAssistantMessage { true } - ) + strategy.edge(AIAgentEdge.builder() + .from(nodeSendToolResult) + .to(strategy.nodeFinish) + .onCondition(msg -> msg instanceof Message.Assistant) + .transformed(msg -> ((Message.Assistant) msg).getContent()) + .build()); + + return strategy.build(); } -} -``` - + ``` + ### Streaming data strategy @@ -871,37 +1458,46 @@ A streaming data strategy is designed for processing streaming data from the LLM streaming data, processes it, and potentially calls tools with the processed data. - -```kotlin -val agentStrategy = strategy>("library-assistant") { - // Describe the node containing the output stream parsing - val getMdOutput by node> { booksDescription -> - val books = mutableListOf() - val mdDefinition = markdownBookDefinition() - - llm.writeSession { - appendPrompt { user(booksDescription) } - // Initiate the response stream in the form of the definition `mdDefinition` - val markdownStream = requestLLMStreaming(mdDefinition) - // Call the parser with the result of the response stream and perform actions with the result - parseMarkdownStreamToBooks(markdownStream).collect { book -> - books.add(book) - println("Parsed Book: ${book.title} by ${book.author}") +=== "Kotlin" + + + ```kotlin + val agentStrategy = strategy>("library-assistant") { + // Describe the node containing the output stream parsing + val getMdOutput by node> { booksDescription -> + val books = mutableListOf() + val mdDefinition = markdownBookDefinition() + + llm.writeSession { + appendPrompt { user(booksDescription) } + // Initiate the response stream in the form of the definition `mdDefinition` + val markdownStream = requestLLMStreaming(mdDefinition) + // Call the parser with the result of the response stream and perform actions with the result + parseMarkdownStreamToBooks(markdownStream).collect { book -> + books.add(book) + println("Parsed Book: ${book.title} by ${book.author}") + } } - } - books + books + } + // Describe the agent's graph making sure the node is accessible + edge(nodeStart forwardTo getMdOutput) + edge(getMdOutput forwardTo nodeFinish) } - // Describe the agent's graph making sure the node is accessible - edge(nodeStart forwardTo getMdOutput) - edge(getMdOutput forwardTo nodeFinish) -} -``` - + ``` + + +=== "Java" + + ```java. + // The framework currently provides streaming APIs primarily through Kotlin's Flow interface. + ``` + diff --git a/docs/docs/snippets/quickstart-snippets.md b/docs/docs/snippets/quickstart-snippets.md index b36cf54611..92ca01197b 100644 --- a/docs/docs/snippets/quickstart-snippets.md +++ b/docs/docs/snippets/quickstart-snippets.md @@ -18,7 +18,7 @@ Add the [Koog package](https://central.sonatype.com/artifact/ai.koog/koog-agents ``` kotlin title="build.gradle.kts" dependencies { - implementation("ai.koog:koog-agents:0.6.4") + implementation("ai.koog:koog-agents:0.7.1") } ``` @@ -26,7 +26,7 @@ Add the [Koog package](https://central.sonatype.com/artifact/ai.koog/koog-agents ``` groovy title="build.gradle" dependencies { - implementation 'ai.koog:koog-agents:0.6.4' + implementation 'ai.koog:koog-agents:0.7.1' } ``` @@ -36,7 +36,7 @@ Add the [Koog package](https://central.sonatype.com/artifact/ai.koog/koog-agents ai.koog koog-agents-jvm - 0.6.4 + 0.7.1 ``` # --8<-- [end:dependencies] diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 95d92a43ea..3d4c608f75 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -137,6 +137,7 @@ plugins: redirect_maps: 'getting-started.md': 'quickstart/index.md' 'simple-api-getting-started.md': 'quickstart/index.md' + 'getting-started/index.md': 'quickstart/index.md' 'advanced-tool-implementation.md': 'class-based-tools.md' 'key-concepts.md': 'glossary.md' 'single-run-agents.md': 'agents/basic-agents.md' diff --git a/examples/javaone-2026/gradle.properties b/examples/javaone-2026/gradle.properties index 2ce3b67598..2da9baf6ff 100644 --- a/examples/javaone-2026/gradle.properties +++ b/examples/javaone-2026/gradle.properties @@ -1 +1 @@ -koogVersion=0.7.1-rc3 \ No newline at end of file +koogVersion=0.7.1-rc3 diff --git a/gradle.properties b/gradle.properties index be90085bce..6f76442ac0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,4 +17,4 @@ kotlin.native.ignoreDisabledTargets=true group=ai.koog # our version follows the semver specification -version=0.7.0 +version=0.7.1