{"id":2347,"date":"2025-08-08T17:17:05","date_gmt":"2025-08-08T17:17:05","guid":{"rendered":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2025\/08\/08\/build-a-recipe-ai-agent-with-koog-and-docker\/"},"modified":"2025-08-08T17:17:05","modified_gmt":"2025-08-08T17:17:05","slug":"build-a-recipe-ai-agent-with-koog-and-docker","status":"publish","type":"post","link":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2025\/08\/08\/build-a-recipe-ai-agent-with-koog-and-docker\/","title":{"rendered":"Build a Recipe AI Agent with Koog and Docker"},"content":{"rendered":"<p>Hi, I\u2019m Philippe Charriere, a Principal Solutions Architect at Docker. I like to test new tools and see how they fit into real-world workflows. Recently, I set out to see if JetBrains\u2019 Koog framework could run with Docker Model Runner, and what started as a quick test turned into something a lot more interesting than I expected. In this new blog post, we\u2019ll explore how to create a small Koog agent specializing in ratatouille recipes using popular Docker AI tools (disclaimer: I\u2019m French). We\u2019ll be using:<\/p>\n<p><a href=\"https:\/\/docs.koog.ai\/\" target=\"_blank\">Koog<\/a>: a framework for building AI Agents in Kotlin<\/p>\n<p><a href=\"https:\/\/docs.docker.com\/ai\/model-runner\/\" target=\"_blank\">Docker Model Runner<\/a>: a Docker feature that allows deploying AI models locally, based on <a href=\"https:\/\/github.com\/ggml-org\/llama.cpp\" target=\"_blank\">Llama.cpp<\/a><\/p>\n<p><a href=\"https:\/\/docs.docker.com\/ai\/compose\/models-and-compose\/\" target=\"_blank\">Agentic Compose<\/a>: a Docker Compose feature to easily integrate AI models into your applications<\/p>\n<p><a href=\"https:\/\/www.docker.com\/blog\/docker-mcp-gateway-secure-infrastructure-for-agentic-ai\/\">Docker MCP Gateway<\/a>: a gateway to access MCP (Model Context Protocol) servers from the <a href=\"https:\/\/hub.docker.com\/mcp\" target=\"_blank\">Docker MCP Catalog<\/a>\u00a0<\/p>\n<h2 class=\"wp-block-heading\">Prerequisites: Kotlin project initialization<\/h2>\n<p>I use <a href=\"https:\/\/www.jetbrains.com\/idea\/\" target=\"_blank\">IntelliJ IDEA Community Edition<\/a> to initialize the Kotlin project.<\/p>\n<p>I use OpenJDK 23 and Gradle Kotlin DSL for project configuration.<\/p>\n<h3 class=\"wp-block-heading\">Step 1: Gradle Configuration<\/h3>\n<p>Here\u2019s my build configuration: build.gradle.kts<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\nplugins {<br \/>\n    kotlin(&#8220;jvm&#8221;) version &#8220;2.1.21&#8221;<br \/>\n    application<br \/>\n}\n<p>group = &#8220;kitchen.ratatouille&#8221;<br \/>\nversion = &#8220;1.0-SNAPSHOT&#8221;<\/p>\n<p>repositories {<br \/>\n    mavenCentral()<br \/>\n}<\/p>\n<p>dependencies {<br \/>\n    testImplementation(kotlin(&#8220;test&#8221;))<br \/>\n    implementation(&#8220;ai.koog:koog-agents:0.3.0&#8221;)<br \/>\n    implementation(&#8220;org.slf4j:slf4j-simple:2.0.9&#8221;)<\/p>\n<p>}<\/p>\n<p>application {<br \/>\n    mainClass.set(&#8220;kitchen.ratatouille.MainKt&#8221;)<br \/>\n}<\/p>\n<p>tasks.test {<br \/>\n    useJUnitPlatform()<br \/>\n}<\/p>\n<p>tasks.jar {<br \/>\n    duplicatesStrategy = DuplicatesStrategy.EXCLUDE<br \/>\n    manifest {<br \/>\n        attributes(&#8220;Main-Class&#8221; to &#8220;kitchen.ratatouille.MainKt&#8221;)<br \/>\n    }<br \/>\n    from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })<br \/>\n}<\/p>\n<p>kotlin {<br \/>\n    jvmToolchain(23)<br \/>\n}<\/p>\n<\/div>\n<h3 class=\"wp-block-heading\">Step 2: Docker Compose Project Configuration<\/h3>\n<p>The new \u201cagentic\u201d feature of Docker Compose allows defining the models to be used by Docker Compose services.<\/p>\n<p>With the content below, I define that I will use the hf.co\/menlo\/lucy-128k-gguf:q4_k_m model from <a href=\"https:\/\/huggingface.co\/Menlo\/Lucy-128k-gguf\" target=\"_blank\">Hugging Face<\/a> for my \u201cKoog agent\u201d.<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\nmodels:<br \/>\n  app_model:<br \/>\n    model: hf.co\/menlo\/lucy-128k-gguf:q4_k_m\n<\/div>\n<p>And I make the \u201clink\u201d between the koog-app service and the app_model model and the Koog agent as follows at the service level:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\nmodels:<br \/>\n      app_model:<br \/>\n        endpoint_var: MODEL_RUNNER_BASE_URL<br \/>\n        model_var: MODEL_RUNNER_CHAT_MODEL\n<\/div>\n<p>Docker Compose will automatically inject the MODEL_RUNNER_BASE_URL and MODEL_RUNNER_CHAT_MODEL environment variables into the koog-app service, which allows the Koog agent to connect to the model.<\/p>\n<p>If you entered interactive mode in the koog-app container, you could verify that the environment variables are properly defined with the command:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\nenv | grep &#8216;^MODEL_RUNNER&#8217;\n<\/div>\n<p>And you would get something like:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\nMODEL_RUNNER_BASE_URL=http:\/\/model-runner.docker.internal\/engines\/v1\/<br \/>\nMODEL_RUNNER_CHAT_MODEL=hf.co\/menlo\/lucy-128k-gguf:q4_k_m\n<\/div>\n<p>It\u2019s entirely possible to define multiple models.<\/p>\n<p>The complete compose.yaml file looks like this:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\nservices:\n<p>  koog-app:<br \/>\n    build:<br \/>\n      context: .<br \/>\n      dockerfile: Dockerfile<br \/>\n    environment:<br \/>\n      SYSTEM_PROMPT: You are a helpful cooking assistant.<br \/>\n      AGENT_INPUT: How to cook a ratatouille?<br \/>\n    models:<br \/>\n      app_model:<br \/>\n        endpoint_var: MODEL_RUNNER_BASE_URL<br \/>\n        model_var: MODEL_RUNNER_CHAT_MODEL<\/p>\n<p>models:<br \/>\n  app_model:<br \/>\n    model: hf.co\/menlo\/lucy-128k-gguf:q4_k_m<\/p>\n<\/div>\n<h3 class=\"wp-block-heading\">Step 3: Dockerfile<\/h3>\n<p>Next, we\u2019ll need a Dockerfile to build the Docker image of our Koog application. The Dockerfile uses multi-stage build to optimize the final image size, so it\u2019s divided into two parts\/stages: one for building the application (build) and one for execution (runtime). Here\u2019s the content of the Dockerfile:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\n# Stage 1: Build<br \/>\nFROM eclipse-temurin:23-jdk-noble AS build\n<p>WORKDIR \/app<\/p>\n<p>COPY gradlew .<br \/>\nCOPY gradle\/ gradle\/<br \/>\nCOPY build.gradle.kts .<br \/>\nCOPY settings.gradle.kts .<\/p>\n<p>RUN chmod +x .\/gradlew<\/p>\n<p>COPY src\/ src\/<\/p>\n<p># Build<br \/>\nRUN .\/gradlew clean build<\/p>\n<p># Stage 2: Runtime<br \/>\nFROM eclipse-temurin:23-jre-noble AS runtime<\/p>\n<p>WORKDIR \/app<\/p>\n<p>COPY &#8211;from=build \/app\/build\/libs\/ratatouille-1.0-SNAPSHOT.jar app.jar<br \/>\nCMD [&#8220;java&#8221;, &#8220;-jar&#8221;, &#8220;app.jar&#8221;]<\/p>\n<\/div>\n<h3 class=\"wp-block-heading\">Step 4: Kotlin side:<\/h3>\n<h4 class=\"wp-block-heading\">Connecting to Docker Model Runner<\/h4>\n<p>Now, here\u2019s the source code of our application, in the src\/main\/kotlin\/Main.kt file to be able to use Docker Model Runner. The API exposed by Docker Model Runner is compatible with the OpenAI API, so we\u2019ll use Koog\u2019s OpenAI client to interact with our model:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\npackage kitchen.ratatouille\n<p>import ai.koog.prompt.executor.clients.openai.OpenAIClientSettings<br \/>\nimport ai.koog.prompt.executor.clients.openai.OpenAILLMClient<\/p>\n<p>suspend fun main() {<\/p>\n<p>    val apiKey = &#8220;nothing&#8221;<br \/>\n    val customEndpoint = System.getenv(&#8220;MODEL_RUNNER_BASE_URL&#8221;).removeSuffix(&#8220;\/&#8221;)<br \/>\n    val model = System.getenv(&#8220;MODEL_RUNNER_CHAT_MODEL&#8221;)<\/p>\n<p>    val client = OpenAILLMClient(<br \/>\n        apiKey=apiKey,<br \/>\n        settings = OpenAIClientSettings(customEndpoint)<br \/>\n    )<br \/>\n}<\/p>\n<\/div>\n<h4 class=\"wp-block-heading\">First Koog Agent<\/h4>\n<p>Creating an agent with Koog is relatively simple as you can see in the code below. We\u2019ll need:<\/p>\n<p>a SingleLLMPromptExecutor that will use the OpenAI client we created previously to execute requests to the model.<\/p>\n<p>an LLModel that will define the model we\u2019re going to use.<\/p>\n<p>an AIAgent that will encapsulate the model and the prompt executor to execute requests.<\/p>\n<p>Regarding the prompt, I use the SYSTEM_PROMPT environment variable to define the agent\u2019s system prompt, and AGENT_INPUT to define the agent\u2019s input (the \u201cuser message\u201d). These variables were defined in the compose.yaml file previously:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\nenvironment:<br \/>\n      SYSTEM_PROMPT: You are a helpful cooking assistant.<br \/>\n      AGENT_INPUT: How to cook a ratatouille?\n<\/div>\n<p>And here\u2019s the complete code of the Koog agent in the src\/main\/kotlin\/Main.kt file:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\npackage kitchen.ratatouille\n<p>import ai.koog.agents.core.agent.AIAgent<br \/>\nimport ai.koog.prompt.executor.clients.openai.OpenAIClientSettings<br \/>\nimport ai.koog.prompt.executor.clients.openai.OpenAILLMClient<br \/>\nimport ai.koog.prompt.executor.llms.SingleLLMPromptExecutor<br \/>\nimport ai.koog.prompt.llm.LLMCapability<br \/>\nimport ai.koog.prompt.llm.LLMProvider<br \/>\nimport ai.koog.prompt.llm.LLModel<\/p>\n<p>suspend fun main() {<\/p>\n<p>    val apiKey = &#8220;nothing&#8221;<br \/>\n    val customEndpoint = System.getenv(&#8220;MODEL_RUNNER_BASE_URL&#8221;).removeSuffix(&#8220;\/&#8221;)<br \/>\n    val model = System.getenv(&#8220;MODEL_RUNNER_CHAT_MODEL&#8221;)<\/p>\n<p>    val client = OpenAILLMClient(<br \/>\n        apiKey=apiKey,<br \/>\n        settings = OpenAIClientSettings(customEndpoint)<br \/>\n    )<\/p>\n<p>    val promptExecutor = SingleLLMPromptExecutor(client)<\/p>\n<p>    val llmModel = LLModel(<br \/>\n        provider = LLMProvider.OpenAI,<br \/>\n        id = model,<br \/>\n        capabilities = listOf(LLMCapability.Completion)<br \/>\n    )<\/p>\n<p>    val agent = AIAgent(<br \/>\n        executor = promptExecutor,<br \/>\n        systemPrompt = System.getenv(&#8220;SYSTEM_PROMPT&#8221;),<br \/>\n        llmModel = llmModel,<br \/>\n        temperature = 0.0<br \/>\n    )<\/p>\n<p>    val recipe = agent.run(System.getenv(&#8220;AGENT_INPUT&#8221;))<\/p>\n<p>    println(&#8220;Recipe:n $recipe&#8221;)<\/p>\n<p>}<\/p>\n<\/div>\n<h2 class=\"wp-block-heading\">Running the project<\/h2>\n<p>All that\u2019s left is to launch the project with the following command:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\ndocker compose up &#8211;build &#8211;no-log-prefix\n<\/div>\n<p>Then wait a moment, depending on your machine, the build and completion times will be more or less long. I nevertheless chose <a href=\"https:\/\/huggingface.co\/Menlo\/Lucy-128k-gguf\" target=\"_blank\">Lucy 128k<\/a> because it can run on small configurations, even without a GPU. This model also has the advantage of being quite good at \u201cfunction calling\u201d detection despite its small size (however, it doesn\u2019t support parallel tool calls). And you should finally get something like this in the console:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\nRecipe:<br \/>\n Sure! Here&#8217;s a step-by-step guide to cooking a classic ratatouille:\n<p>&#8212;<\/p>\n<p>### **Ingredients**<br \/>\n&#8211; 2 boneless chicken thighs or 1-2 lbs rabbit (chicken is common, but rabbit is traditional)<br \/>\n&#8211; 1 small onion (diced)<br \/>\n&#8211; 2 garlic cloves (minced)<br \/>\n&#8211; 1 cup tomatoes (diced)<br \/>\n&#8211; 1 zucchini (sliced)<br \/>\n&#8211; 1 yellow squash or eggplant (sliced)<br \/>\n&#8211; 1 bell pepper (sliced)<br \/>\n&#8211; 2 medium potatoes (chopped)<br \/>\n&#8211; 1 red onion (minced)<br \/>\n&#8211; 2 tbsp olive oil<br \/>\n&#8211; 1 tbsp thyme (or rosemary)<br \/>\n&#8211; Salt and pepper (to taste)<br \/>\n&#8211; Optional: 1\/4 cup wine (white or red) to deglaze the pan  <\/p>\n<p>&#8212;<\/p>\n<p>### **Steps**<br \/>\n1. **Prep the Ingredients**<br \/>\n   &#8211; Dice the onion, garlic, tomatoes, zucchini, squash, bell pepper, potatoes.<br \/>\n   &#8211; Saut\u00e9 the chicken in olive oil until browned (about 10\u201315 minutes).<br \/>\n   &#8211; Add the onion and garlic, saut\u00e9 for 2\u20133 minutes.  <\/p>\n<p>2. **Add Vegetables &amp; Flavor**<br \/>\n   &#8211; Pour in the tomatoes, zucchini, squash, bell pepper, red onion, and potatoes.<br \/>\n   &#8211; Add thyme, salt, pepper, and wine (if using). Stir to combine.<br \/>\n   &#8211; Add about 1 cup water or stock to fill the pot, if needed.  <\/p>\n<p>3. **Slow Cook**<br \/>\n   &#8211; Place the pot in a large pot of simmering water (or use a Dutch oven) and cook on low heat (around 200\u00b0F\/90\u00b0C) for about 30\u201340 minutes, or until the chicken is tender.<br \/>\n   &#8211; Alternatively, use a stovetop pot with a lid to cook the meat and vegetables together, simmering until the meat is cooked through.  <\/p>\n<p>4. **Finish &amp; Serve**<br \/>\n   &#8211; Remove the pot from heat and let it rest for 10\u201315 minutes to allow flavors to meld.<br \/>\n   &#8211; Stir in fresh herbs (like rosemary or parsley) if desired.<br \/>\n   &#8211; Serve warm with crusty bread or on the plate as is.  <\/p>\n<p>&#8212;<\/p>\n<p>### **Tips**<br \/>\n&#8211; **Meat Variations**: Use duck or other meats if you don&#8217;t have chicken.<br \/>\n&#8211; **Vegetables**: Feel free to swap out any vegetables (e.g., mushrooms, leeks).<br \/>\n&#8211; **Liquid**: If the mixture is too dry, add a splash of water or stock.<br \/>\n&#8211; **Serving**: Ratatouille is often eaten with bread, so enjoy it with a side of crusty bread or a simple salad.  <\/p>\n<p>Enjoy your meal! <\/p>\n<\/div>\n<p>As you can see, it\u2019s quite simple to create an agent with Koog and Docker Model Runner!\u00a0<\/p>\n<p>But we have a problem, I told you I was French and the ratatouille recipe proposed by <a href=\"https:\/\/huggingface.co\/Menlo\/Lucy-128k-gguf\" target=\"_blank\">Lucy 128k<\/a> doesn\u2019t really suit me: there\u2019s no rabbit, chicken, or duck in a ratatouille!!!. But let\u2019s see how to fix that.<\/p>\n<h2 class=\"wp-block-heading\">Let\u2019s add superpowers to our Koog agent with the Docker MCP Gateway<\/h2>\n<p>What I\u2019d like to do now is have my application first search for information about ratatouille ingredients, and then have the Koog agent use this information to improve the recipe. For this, I\u2019d like to use the DuckDuckGo MCP server that\u2019s available on the <a href=\"https:\/\/hub.docker.com\/mcp\/server\/duckduckgo\/overview\" target=\"_blank\">Docker MCP Hub<\/a>. And to make my life easier, I\u2019m going to use the <a href=\"https:\/\/www.docker.com\/blog\/docker-mcp-gateway-secure-infrastructure-for-agentic-ai\/\">Docker MCP Gateway<\/a> to access this MCP server.<\/p>\n<h3 class=\"wp-block-heading\">Configuring the Docker MCP Gateway in Docker Compose<\/h3>\n<p>To use the Docker MCP Gateway, I\u2019ll first modify the compose.yml file to add the gateway configuration.<\/p>\n<h4 class=\"wp-block-heading\">Configuring the gateway in the compose.yaml file<\/h4>\n<p>Here\u2019s the configuration I added for the gateway in the compose.yaml file:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\n mcp-gateway:<br \/>\n    image: docker\/mcp-gateway:latest<br \/>\n    command:<br \/>\n      &#8211; &#8211;port=8811<br \/>\n      &#8211; &#8211;transport=sse<br \/>\n      &#8211; &#8211;servers=duckduckgo<br \/>\n      &#8211; &#8211;verbose<br \/>\n    volumes:<br \/>\n      &#8211; \/var\/run\/docker.sock:\/var\/run\/docker.sock\n<\/div>\n<p>This configuration will create an mcp-gateway service that will listen on port 8811 and use the sse (Server-Sent Events) transport to communicate with MCP servers.<\/p>\n<p><strong>Important<\/strong>:<\/p>\n<p>with \u2013servers=duckduckgo I can filter the available MCP servers to only use the DuckDuckGo server.<\/p>\n<p>the MCP Gateway will automatically pull the available MCP servers from the Docker MCP Hub.<\/p>\n<p>The MCP Gateway is an open-source project that you can find <a href=\"https:\/\/github.com\/docker\/mcp-gateway\" target=\"_blank\">here<\/a>:\u00a0<\/p>\n<p>Next, I\u2019ll modify the koog-app service so it can communicate with the gateway by adding the MCP_HOST environment variable that will point to the gateway URL, as well as the dependency on the mcp-gateway service:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\nenvironment:<br \/>\n      MCP_HOST: http:\/\/mcp-gateway:8811\/sse<br \/>\n    depends_on:<br \/>\n      &#8211; mcp-gateway\n<\/div>\n<p>I\u2019ll also modify the system prompt and user message:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\nenvironment:<br \/>\n      SYSTEM_PROMPT: |<br \/>\n        You are a helpful cooking assistant.<br \/>\n        Your job is to understand the user prompt and decide if you need to use tools to run external commands.<br \/>\n      AGENT_INPUT: |<br \/>\n        Search for the ingredients to cook a ratatouille, max result 1<br \/>\n        Then, from these found ingredients, generate a yummy ratatouille recipe<br \/>\n        Do it only once\n<\/div>\n<p>So here\u2019s the complete compose.yml file with the MCP Gateway configuration and the modifications made to the koog-app service:<\/p>\n\n<div class=\"wp-block-syntaxhighlighter-code \">\nservices:\n<p>  koog-app:<br \/>\n    build:<br \/>\n      context: .<br \/>\n      dockerfile: Dockerfile<br \/>\n    environment:<br \/>\n      SYSTEM_PROMPT: |<br \/>\n        You are a helpful cooking assistant.<br \/>\n        Your job is to understand the user prompt and decide if you need to use tools to run external commands.<br \/>\n      AGENT_INPUT: |<br \/>\n        Search for the ingredients to cook a ratatouille, max result 1<br \/>\n        Then, from these found ingredients, generate a yummy ratatouille recipe<br \/>\n        Do it only once<br \/>\n      MCP_HOST: http:\/\/mcp-gateway:8811\/sse<br \/>\n    depends_on:<br \/>\n      &#8211; mcp-gateway<br \/>\n    models:<br \/>\n      app_model:<br \/>\n        # NOTE: populate the environment variables with the model runner endpoint and model name<br \/>\n        endpoint_var: MODEL_RUNNER_BASE_URL<br \/>\n        model_var: MODEL_RUNNER_CHAT_MODEL<\/p>\n<p>  mcp-gateway:<br \/>\n    image: docker\/mcp-gateway:latest<br \/>\n    command:<br \/>\n      &#8211; &#8211;port=8811<br \/>\n      &#8211; &#8211;transport=sse<br \/>\n      &#8211; &#8211;servers=duckduckgo<br \/>\n      &#8211; &#8211;verbose<br \/>\n    volumes:<br \/>\n      &#8211; \/var\/run\/docker.sock:\/var\/run\/docker.sock<\/p>\n<p>models:<br \/>\n  app_model:<br \/>\n    model: hf.co\/menlo\/lucy-128k-gguf:q4_k_m\n<\/p><\/div>\n<p>Now, let\u2019s modify the Kotlin code to use the MCP Gateway and search for ratatouille ingredients.<\/p>\n<h3 class=\"wp-block-heading\">Modifying the Kotlin code to use the MCP Gateway<\/h3>\n<p>The modification is extremely simple; you just need to:<\/p>\n<p>define the MCP transport (SseClientTransport) with the gateway URL: val transport = McpToolRegistryProvider.defaultSseTransport(System.getenv(\u201cMCP_HOST\u201d))<\/p>\n<p>create the MCP tools registry with the gateway: val toolRegistry = McpToolRegistryProvider.fromTransport(transport = transport, name = \u201csse-client\u201d, version = \u201c1.0.0\u201d)<\/p>\n<p>and finally, add the tools registry to the Koog agent constructor: toolRegistry = toolRegistry<\/p>\n<p><strong>Extremely important<\/strong>: I added capabilities = listOf(LLMCapability.Completion, LLMCapability.Tools) for the LLM model, because we\u2019re going to use its \u201cfunction calling\u201d capabilities (the tools are defined and provided by the MCP server).<\/p>\n<p>Here\u2019s the complete code of the Koog agent modified to use the MCP Gateway in the src\/main\/kotlin\/Main.kt file:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\npackage kitchen.ratatouille\n<p>import ai.koog.agents.core.agent.AIAgent<br \/>\nimport ai.koog.agents.mcp.McpToolRegistryProvider<br \/>\nimport ai.koog.prompt.executor.clients.openai.OpenAIClientSettings<br \/>\nimport ai.koog.prompt.executor.clients.openai.OpenAILLMClient<br \/>\nimport ai.koog.prompt.executor.llms.SingleLLMPromptExecutor<br \/>\nimport ai.koog.prompt.llm.LLMCapability<br \/>\nimport ai.koog.prompt.llm.LLMProvider<br \/>\nimport ai.koog.prompt.llm.LLModel<\/p>\n<p>suspend fun main() {<\/p>\n<p>    val transport = McpToolRegistryProvider.defaultSseTransport(System.getenv(&#8220;MCP_HOST&#8221;))<br \/>\n    \/\/ Create a tool registry with tools from the MCP server<br \/>\n    val toolRegistry = McpToolRegistryProvider.fromTransport(<br \/>\n        transport = transport,<br \/>\n        name = &#8220;sse-client&#8221;,<br \/>\n        version = &#8220;1.0.0&#8221;<br \/>\n    )<br \/>\n    println(toolRegistry.tools)<\/p>\n<p>    val apiKey = &#8220;nothing&#8221;<br \/>\n    val customEndpoint = System.getenv(&#8220;MODEL_RUNNER_BASE_URL&#8221;).removeSuffix(&#8220;\/&#8221;)<br \/>\n    val model = System.getenv(&#8220;MODEL_RUNNER_CHAT_MODEL&#8221;)<\/p>\n<p>    val client = OpenAILLMClient(<br \/>\n        apiKey=apiKey,<br \/>\n        settings = OpenAIClientSettings(customEndpoint)<br \/>\n    )<\/p>\n<p>    val promptExecutor = SingleLLMPromptExecutor(client)<\/p>\n<p>    val llmModel = LLModel(<br \/>\n        provider = LLMProvider.OpenAI,<br \/>\n        id = model,<br \/>\n        capabilities = listOf(LLMCapability.Completion, LLMCapability.Tools)<br \/>\n    )<\/p>\n<p>    val agent = AIAgent(<br \/>\n        executor = promptExecutor,<br \/>\n        systemPrompt = System.getenv(&#8220;SYSTEM_PROMPT&#8221;),<br \/>\n        llmModel = llmModel,<br \/>\n        temperature = 0.0,<br \/>\n        toolRegistry = toolRegistry<br \/>\n    )<\/p>\n<p>    val recipe = agent.run(System.getenv(&#8220;AGENT_INPUT&#8221;))<\/p>\n<p>    println(&#8220;Recipe:n $recipe&#8221;)<\/p>\n<p>}<\/p>\n<\/div>\n<h3 class=\"wp-block-heading\">Launching the project with the MCP Gateway<\/h3>\n<p>Let\u2019s launch the project again with the command:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\ndocker compose up &#8211;build &#8211;no-log-prefix\n<\/div>\n<p>And after a while, you should get a new ratatouille recipe, but the LLM will have relied on the search results performed by the DuckDuckGo MCP server (via the MCP Gateway) to improve the recipe. The response time will be a bit longer because the LLM will first query the MCP server to get the ratatouille ingredients, then it will generate the recipe. And the DuckDuckGo MCP server will search for links and then retrieve the content of those links (indeed, the DuckDuckGo MCP server exposes 2 tools: search and fetch_content).<\/p>\n<p>Here\u2019s an example of what you might get with an improved and more \u201cauthentic\u201d ratatouille recipe:<\/p>\n<div class=\"wp-block-syntaxhighlighter-code \">\nRecipe:<br \/>\n Here&#8217;s a **complete and easy-to-follow version** of **Ratatouille**, based on the recipe you provided, with tips and variations to suit your preferences:\n<p>&#8212;<\/p>\n<p>###  **What Is Ratatouille?**<br \/>\nA classic French vegetable stew, traditionally made with eggplant, tomatoes, zucchini, bell peppers, onions, and mushrooms. It&#8217;s often seasoned with herbs like parsley, thyme, or basil and paired with crusty bread or a light sauce.<\/p>\n<p>&#8212;<\/p>\n<p>###  **Ingredients** (for 4 servings):<br \/>\n&#8211; **1\/2 cup olive oil** (divided)<br \/>\n&#8211; **2 tbsp olive oil** (for the skillet)<br \/>\n&#8211; **3 cloves garlic**, minced<br \/>\n&#8211; **1 eggplant**, cubed<br \/>\n&#8211; **2 zucchinis**, sliced<br \/>\n&#8211; **2 large tomatoes**, chopped<br \/>\n&#8211; **2 cups fresh mushrooms**, sliced<br \/>\n&#8211; **1 large onion**, sliced<br \/>\n&#8211; **1 green or red bell pepper**, sliced<br \/>\n&#8211; **1\/2 tsp dried parsley**<br \/>\n&#8211; **Salt to taste**<br \/>\n&#8211; **1\/2 cup grated Parmesan cheese** (or pecorino, as you mentioned)  <\/p>\n<p>&#8212;<\/p>\n<p>###  **How to Make Ratatouille**<br \/>\n**Preheat oven** to 350\u00b0F (175\u00b0C).  <\/p>\n<p>1. **Prepare the dish**: Coat a 1\u00bd-quart casserole dish with 1 tbsp olive oil.<br \/>\n2. **Cook the base**: In a skillet, saut\u00e9 garlic until fragrant (about 1\u20132 minutes). Add eggplant, parsley, and salt; cook for 10 minutes until tender.<br \/>\n3. **Layer the vegetables**: Spread the eggplant mixture in the dish, then add zucchini, tomatoes, mushrooms, onion, and bell pepper. Top with Parmesan.<br \/>\n4. **Bake**: Cover and bake for 45 minutes. Check for tenderness; adjust time if needed.  <\/p>\n<p>**Cook&#8217;s Note**:<br \/>\n&#8211; Add mushrooms (optional) or omit for a traditional flavor.<br \/>\n&#8211; Use fresh herbs like thyme or basil if preferred.<br \/>\n&#8211; Substitute zucchini with yellow squash or yellow bell pepper for color.  <\/p>\n<p>&#8212;<\/p>\n<p>###  **How to Serve**<br \/>\n&#8211; **Main dish**: Serve with crusty French bread or rice.<br \/>\n&#8211; **Side**: Pair with grilled chicken or fish.<br \/>\n&#8211; **Guilt-free twist**: Add black olives or a sprinkle of basil\/others for a lighter version.  <\/p>\n<p>&#8212;<\/p>\n<\/div>\n<h3 class=\"wp-block-heading\">Conclusion<\/h3>\n<p>This blog post perfectly illustrates the modern containerized AI ecosystem that Docker is building. By combining <strong>Docker Model Runner<\/strong>, <strong>Agentic Compose<\/strong>, <strong>Docker MCP Gateway,<\/strong> and the <strong>Koog<\/strong> framework (but we could of course use other frameworks), we were able to create an \u201cintelligent\u201d agent quite simply.<\/p>\n<p><strong>Docker Model Runner<\/strong> allowed us to use an AI model locally.<\/p>\n<p><strong>Agentic Compose<\/strong> simplified the integration of the model into our application by automatically injecting the necessary environment variables.<\/p>\n<p>The <strong>Docker MCP Gateway<\/strong> transformed our little agent into a system capable of interacting with the outside world.<\/p>\n<p>The <strong>Koog<\/strong> framework allowed us to orchestrate these components in Kotlin.<\/p>\n<p>Soon, I\u2019ll go deeper into the MCP Gateway and how to use it with your own MCP servers, and not just with Koog. And I continue my explorations with Koog and Docker Model Runner. Check out the entire source code of this project is available <a href=\"https:\/\/github.com\/Short-Compendium\/docker-model-runner-with-koog\/tree\/main\/ratatouille\" target=\"_blank\">here<\/a>\u00a0<\/p>\n<h3 class=\"wp-block-heading\">Learn more<\/h3>\n<p>If you need more GPUs to experiment with different models, sign up for <a href=\"https:\/\/www.docker.com\/products\/docker-offload\/#earlyaccess\">Docker Offload beta program<\/a> and get 300 minutes for free.\u00a0<\/p>\n<p>Discover hundreds of curated MCP servers on the <a href=\"https:\/\/hub.docker.com\/mcp\" target=\"_blank\">Docker MCP Catalog<\/a><\/p>\n<p>Learn more about <a href=\"https:\/\/docs.docker.com\/ai\/mcp-catalog-and-toolkit\/toolkit\/\" target=\"_blank\">Docker MCP Toolkit<\/a><\/p>\n<p>Explore <a href=\"https:\/\/github.com\/docker\/mcp-gateway\" target=\"_blank\">Docker MCP Gateway<\/a> on GitHub<\/p>\n<p>Get started with <a href=\"https:\/\/www.docker.com\/products\/model-runner\/\">Docker Model Runner<\/a><\/p>\n<p>Get more practical agent examples from <a href=\"https:\/\/github.com\/docker\/compose-for-agents\" target=\"_blank\">Agentic Compose<\/a> repos<\/p>","protected":false},"excerpt":{"rendered":"<p>Hi, I\u2019m Philippe Charriere, a Principal Solutions Architect at Docker. I like to test new tools and see how they [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":0,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[4],"tags":[],"class_list":["post-2347","post","type-post","status-publish","format-standard","hentry","category-docker"],"_links":{"self":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/2347","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/comments?post=2347"}],"version-history":[{"count":0,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/2347\/revisions"}],"wp:attachment":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media?parent=2347"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/categories?post=2347"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/tags?post=2347"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}