{"slug": "writing-ai-agents-in-yaml-instead-of-python", "title": "Writing AI agents in YAML instead of Python", "summary": "Docker released docker-agent, an open-source tool that lets developers define AI agents entirely in a single YAML file rather than Python code, and it ships with the `docker agent` command in Docker Desktop 4.63 and later or via `brew install docker-agent`. The tool provides 30 built-in toolsets, including `rag`, `shell`, `filesystem`, `fetch`, `mcp` and `memory`, and its `rag` toolset handles indexing and vector storage automatically, storing embeddings in a local SQLite file and re-indexing changed files. In the walkthrough, an agent configured with `openai/gpt-5-mini` and `openai/text-embedding-3-small` at 1536 vector dimensions searches 72 Polars user-guide markdown files and is run with `docker agent run --exec embeddings.yaml`.", "body_md": "## Table of Contents\n\n## Introduction\n\nThere are plenty of frameworks for building AI agents, such as [LangChain](https://codecut.ai/langchain-1-0-middleware-production-agents/) or LlamaIndex. But as you add models, tools, and features, the agent quickly grows into a lot of code to maintain. More code means more places for mistakes when you need to make a change.\n\ndocker-agent keeps the entire workflow in one YAML file. With this approach, you can mix and match models, tools, and features, and see how the whole agent is set up at a glance.\n\nIn this article, you will learn nice features of docker-agent by building an agent that answers questions about Polars.\n\n💻 **Get the Code**: The agent configs and eval files are on [GitHub](https://github.com/khuyentran1401/codecut-blog/tree/main/docker-agent-docs-qa).\n\n## What Is docker-agent?\n\n[docker-agent](https://github.com/docker/docker-agent) is Docker’s open-source tool for building and running AI agents.\n\ndocker-agent does for AI agents what Docker did for apps. Instead of setting up an environment and writing code, you describe the agent in one YAML file and run it with one command.\n\n### Stay Current with CodeCut\n\nEasy-to-digest articles on Python, AI, and open-source tools. Delivered twice a week.\n\n## Setup\n\nInstall docker-agent with Homebrew, or use the `docker agent` command that ships with Docker Desktop 4.63 and later:\n\n```\nbrew install docker-agent\n```\n\nThe agent will search the [Polars user guide](https://github.com/pola-rs/polars/tree/main/docs/source/user-guide) and answer questions about it.\n\nThe project folder looks like this:\n\n```\ndocker-agent-docs-qa/\n└── docs/polars/    # 72 markdown files the agent will search\n```\n\n## Create the Agent\n\nTo create the agent, define its model, instructions, and tools in a YAML file called `embeddings.yaml`. Start with the `agents` section.\n\n```\nagents:\n  root:    # the agent that receives your message\n    model: openai/gpt-5-mini\n    description: Answers questions about Polars from the user guide\n    instruction: |\n      You answer questions about the Polars DataFrame library.\n      Always search the knowledge base before answering.\n      Answer only from what the search returns.\n      End with a \"Sources:\" list of the file paths you used, relative to docs/polars.\n```\n\nTo let the agent search the docs, give it a `rag` tool and tell the tool which folder to search.\n\n```\nagents:\n  root:\n    ...\n    toolsets:\n      - type: rag\n        ref: polars_docs          # use the source defined below\n\nrag:\n  polars_docs:                    # the name the ref points to\n    tool:\n      description: The Polars user guide\n    docs: [./docs/polars]         # what to search\n    strategies:                   # how to search\n      - type: chunked-embeddings  # split each file into chunks and embed them\n        embedding_model: openai/text-embedding-3-small\n        vector_dimensions: 1536   # number of values per vector\n```\n\nRAG setups usually need a vector database and a separate indexing step. docker-agent handles both for you. It stores the vectors in a local SQLite file, indexes the folder the first time the agent starts, and re-indexes files when they change.\n\n`rag` is one of 30 built-in toolsets. Here are a few others that are useful:\n\n- `shell` : run shell commands, such as a Python script or`duckdb`\n- `filesystem` : read, write, and search files in a folder\n- `fetch` : read web pages and online docs\n- `mcp` : call external services like GitHub or a[database through MCP](https://codecut.ai/fastmcp-sql-bridge-database-access/)\n- `memory` : remember facts across sessions\n\nAfter the YAML file is created, ask the agent a question. The `--exec` flag runs the agent without opening the interactive terminal UI.\n\n```\ndocker agent run --exec embeddings.yaml \"How do I read a Parquet file without loading it all into memory?\"\n```\n\nThe output shows the search the agent made, followed by its answer. The search results and the model’s reasoning summary are trimmed here:\n\n```\nCalling polars_docs(query: \"read Parquet without loading all into memory scan_parquet\nscan_parquet streaming lazy read_parquet memory efficient 'scan_parquet' 'read_parquet'\n'scan' 'Parquet'\")\n\nUse a lazy scan and streaming collect so Polars parses and processes the file in\nbatches instead of materializing everything in RAM.\n\n- Use pl.scan_parquet(...) (returns a LazyFrame) instead of pl.read_parquet(...).\n  Scanning delays parsing and enables column/row pruning via the optimizer\n  (projection & predicate pushdown).\n- Apply selects/filters on the LazyFrame so the reader can skip unnecessary\n  columns/rows.\n- When collecting, use streaming execution to process in batches:\n  .collect(engine=\"streaming\").\n\nSources:\n- io/parquet.md\n- lazy/sources_sinks.md\n- lazy/execution.md\n```\n\nThe answer is correct, and every claim traces back to a file you can open.\n\nTo ask another question, run the same command with a new question without changing the YAML file.\n\n```\ndocker agent run --exec embeddings.yaml \"When should I use join_asof?\"\n```\n\n## Swap the Model\n\nTo try a cheaper model without committing to it, you can simply pass `--model` for a single run.\n\n```\n embeddings.yaml          --model           what runs\n┌───────────────────┐                     ┌───────────────────┐\n│ model: gpt-5-mini │ ── gpt-5-nano ──>   │ model: gpt-5-nano │\n│ instruction: ...  │                     │ instruction: ...  │\n│ toolsets: rag     │                     │ toolsets: rag     │\n└───────────────────┘                     └───────────────────┘\n     unchanged                               this run only\n```\n\nFor example, this runs the same question on `gpt-5-nano`.\n\n```\ndocker agent run --exec embeddings.yaml --model openai/gpt-5-nano \\\n  \"How do I read a Parquet file without loading it all into memory?\"\n- Use scan_parquet to read lazily. This returns a LazyFrame and does not parse the\n  data into memory until you collect.\n...\nSources:\nio/parquet.md\n```\n\nThis is very nice for several reasons:\n\n- You can quickly try different models and compare their answers\n- Because the YAML file does not change during the experiment, your teammates are not affected\n- Once you like a model, you can edit the YAML file to use it permanently\n\n## Reuse Instructions Across Questions\n\nYou might want to vary the instructions based on the context, such as the audience, the length, or the tone.\n\nWith docker-agent, you can write different instructions in separate files, then pick one with the `--prompt-file` flag.\n\nFor example, you can create a `pandas-users.md` file with the instructions for pandas users and a `sql-users.md` file with the instructions for SQL users.\n\n```\n \"How do I read a Parquet file without loading it all into memory?\"\n                               │\n           ┌───────────────────┴───────────────────┐\n           ▼                                       ▼\n --prompt-file pandas-users.md          --prompt-file sql-users.md\n           │                                       │\n           ▼                                       ▼\n Pandas: read_parquet() loads          SQL: SELECT * FROM\n the file into memory                  read_parquet('data.parquet')\n```\n\nLet’s try the `pandas-users.md` file first. Start by creating the file.\n\n```\nThe reader knows pandas. Show the pandas equivalent next to each Polars answer.\nKeep each answer under 8 lines.\n```\n\nThen start an interactive chat with the file passed to `--prompt-file`.\n\n```\ndocker agent run embeddings.yaml --prompt-file pandas-users.md\n```\n\nNow every answer follows the instructions in `pandas-users.md`, even though the questions never mention pandas.\n\n```\n> How do I read a Parquet file without loading it all into memory?\n\nPolars: use scan_parquet(\"file.parquet\") to get a LazyFrame (parsing is delayed).\nCollect with streaming to process in batches: lf.collect(engine=\"streaming\").\n...\nPandas: read_parquet(\"file.parquet\") loads the file into memory (no built-in\nlazy/streaming scan).\n```\n\nIf your audience knows SQL instead, create a prompt file called `sql-users.md`.\n\n```\nThe reader knows SQL. Show the Polars SQL equivalent next to each Polars answer.\nKeep each answer under 8 lines.\n```\n\nStart a chat with that file instead.\n\n```\ndocker agent run embeddings.yaml --prompt-file sql-users.md\n```\n\nThe same question now gets a SQL answer instead of a pandas one.\n\n```\n> How do I read a Parquet file without loading it all into memory?\n\n- Polars: Use lazy scanning so the file isn't parsed upfront:\n  lf = pl.scan_parquet(\"data.parquet\"), which returns a LazyFrame.\n  SQL: SELECT * FROM read_parquet('data.parquet');\n- Polars: Push down projections/filters so only needed columns/rows are read.\n  SQL: SELECT col1, col2 FROM read_parquet('data.parquet') WHERE ...;\n...\n```\n\n## Add Variants with Flavors\n\nSometimes you want more than one version of an agent, such as one with a different search method or a different model. It is redundant to write another YAML file for each version with most of the settings staying the same.\n\nWith docker-agent, you can add a flavor to the same file. A flavor lists only the settings that change, and everything else stays the same.\n\n```\n               --flavor hybrid\n            off               on\n             │                 │\n             ▼                 ▼\n   ┌──────────────────┐  ┌──────────────────┐\n   │ embedding search │  │ embedding search │\n   │                  │  │ + keyword search │\n   └──────────────────┘  └──────────────────┘\n       base config         base + hybrid\n```\n\nIn this example, we add a flavor called `hybrid` that adds keyword search to the existing embedding search strategy.\n\n```\n# embeddings.yaml\nagents:\n  root: ...\n\nrag:\n  polars_docs:\n    ...\n    strategies:\n      - type: chunked-embeddings  # embedding search\n\nflavors:                          # new section at the bottom of the file\n  hybrid:\n    rag:\n      polars_docs:\n        strategies+:              # keep chunked-embeddings and add bm25\n          - type: bm25            # keyword search\n        results:\n          fusion:\n            strategy: rrf         # merge the keyword and embedding results\n            k: 60\n          deduplicate: true\n          limit: 5\n```\n\nTo use a flavor, pass its name with `--flavor`:\n\n```\n# embedding search only\ndocker agent run embeddings.yaml\n\n# embedding search + keyword search\ndocker agent run embeddings.yaml --flavor hybrid\n```\n\nHere is what the config looks like after docker-agent applies the `hybrid` flavor.\n\n```\nrag:\n  polars_docs:\n    ...\n    strategies:\n      - type: chunked-embeddings      # from the base file\n      - type: bm25                    # from hybrid\n    results:\n      fusion:\n        strategy: rrf                 # from hybrid\n      ...\n```\n\nTo see how this is useful, ask about `SQLContext`, a class name that appears in the docs. Without the flavor, the agent uses the embedding search strategy and finds nothing.\n\n```\ndocker agent run --exec embeddings.yaml \"What is SQLContext?\"\nCalling polars_docs(query: \"SQLContext\")\nCalling polars_docs(query: \"SQL\")\n\nI searched the Polars documentation for \"SQLContext\" (and for \"SQL\") but found\nno matching documents, so I cannot provide a definition from the docs.\n```\n\nWith the `hybrid` flavor, every search runs both strategies. Keyword search finds the exact name, so the right page comes back.\n\n```\ndocker agent run --exec embeddings.yaml --flavor hybrid \"What is SQLContext?\"\nCalling polars_docs(query: \"SQLContext\")\n\nSQLContext is the Polars object that manages SQL queries...\n\nSources:\n- sql/intro.md\n```\n\n## Run Different Agents from One File\n\nSometimes you might want to have multiple agents for different purposes. For example, you might want to have a small model for answering questions and a stronger model for reviewing code.\n\ndocker-agent lets you add more agents to the same file. They can share tools already defined there, so you don’t need to repeat the configuration.\n\n```\n       root                               reviewer\n ┌───────────────────┐              ┌───────────────────┐\n │ gpt-5-mini        │              │ gpt-5             │\n │ answers questions │              │ reviews code      │\n └─────────┬─────────┘              └─────────┬─────────┘\n           │                                  │\n           └──────────────┐   ┌───────────────┘\n                          ▼   ▼\n                  ┌─────────────────┐\n                  │  polars_docs    │\n                  │  defined once   │\n                  └─────────────────┘\n```\n\nIn the code below, we add a second agent called `reviewer` that uses a stronger model for reviewing code but uses the same `polars_docs` tool as the `root` agent.\n\n```\nagents:\n  root:\n    model: openai/gpt-5-mini        # small model for answering questions\n    instruction: |\n      You answer questions about the Polars DataFrame library.\n      ...\n    toolsets:\n      - type: rag\n        ref: polars_docs          # shared search\n\n  reviewer:\n    model: openai/gpt-5             # stronger model for the harder task\n    instruction: |\n      You review Polars code.\n      Search the docs for each function the code uses.\n      Point out slower or outdated patterns and suggest the fix the docs recommend.\n      ...\n    toolsets:\n      - type: rag\n        ref: polars_docs          # same shared search\n```\n\nBy default, docker-agent runs the first agent in the file. To run the reviewer instead, pass `--agent`.\n\n```\ndocker agent run --exec embeddings.yaml --agent reviewer \\\n  'Review this code: df = pl.read_parquet(\"big.parquet\").filter(pl.col(\"amount\") > 0).select([\"id\", \"amount\"])'\nFindings:\n- Using read_parquet eagerly loads the entire file before your filter/select are\n  applied. This prevents predicate and projection pushdown, which is slower and\n  uses more memory on large Parquet files.\n\nRecommended fix:\n- Switch to scan_parquet and collect at the end:\n  df = (\n      pl.scan_parquet(\"big.parquet\")\n        .filter(pl.col(\"amount\") > 0)\n        .select([\"id\", \"amount\"])\n        .collect()\n  )\n...\n```\n\nThe reviewer spots the eager `read_parquet` call and suggests switching to `scan_parquet`.\n\n## Test the Agent\n\nAs you experiment with different models and settings, you might want to make sure the agent keeps working as expected. It can be easy to miss mistakes by just observing the output.\n\ndocker-agents provides an easy way to define the test cases and run them against the agent.\n\n```\n evals/streaming-parquet.json\n ┌──────────────────────────────────────────────────┐\n │ question: How do I read a Parquet file without   │\n │           loading it all into memory?            │\n └────────────────────────┬─────────────────────────┘\n                          ▼\n                   ┌─────────────┐\n                   │    agent    │\n                   └──────┬──────┘\n                          ▼\n                       answer\n                          │\n       ┌──────────────────┼──────────────────┐\n       ▼                  ▼                  ▼\n ✓ uses               ✓ mentions         ✓ searched\n   scan_parquet         streaming          the docs\n```\n\nTo define a test case, create a JSON file in the `evals/` folder, such as `evals/streaming-parquet.json`.\n\n```\n{\n  \"title\": \"Large Parquet file question\",\n  \"messages\": [\n    {\"message\": {\"message\": {\"role\": \"user\", \"content\": \"How do I read a Parquet file without loading it all into memory?\"}}}\n  ],\n  \"evals\": {\n    \"assertions\": [\n      { \"name\": \"uses scan_parquet\", \"type\": \"contains\", \"value\": \"scan_parquet\" },\n      { \"name\": \"mentions streaming\", \"type\": \"contains\", \"value\": \"streaming\" },\n      { \"name\": \"searched the docs\", \"type\": \"tool_called\", \"value\": \"polars_docs\" }\n    ]\n  }\n}\n```\n\nFor this question to pass, the answer must mention `scan_parquet` and `streaming`, and the agent must have searched the docs.\n\nNow run the suite with `docker agent eval`:\n\n```\ndocker agent eval embeddings.yaml ./evals\n✓ Asof join question ($0.001918)\n  ✓ assertions 3/3\n✓ Large Parquet file question ($0.002629)\n  ✓ assertions 3/3\n[██████████] 100% (2/2) ✓2 ✗0\n\n✅     Assertions: 6/6 passed (100.0%)\n\nTotal Cost: $0.004546\nTotal Time: 26s\n```\n\nEach run reports which tests passed, what each answer cost, and how long it took. Together, they tell you whether a change broke the agent, made it pricier, or slowed it down.\n\nSince each eval runs in its own container, one test can’t leave files behind that change another’s result.\n\n### Compare variants before you switch\n\nBecause `eval` takes `--flavor` too, you can test a variant before adopting it. Let’s say you want to know whether the cheaper `gpt-5-nano` is good enough for your use case. Add it as a second flavor called `cheap`.\n\n```\nflavors:\n  hybrid: ...\n  cheap:\n    agents:\n      root:\n        model: openai/gpt-5-nano  # smallest, lowest-cost GPT-5 model\n```\n\nThen compare the default model against the `cheap` flavor. Since [answers vary between runs](https://codecut.ai/prompt-tweak-score-gain/), `--repeat 3` runs each test three times.\n\n```\ndocker agent eval embeddings.yaml ./evals --repeat 3\ndocker agent eval embeddings.yaml ./evals --flavor cheap --repeat 3\ngpt-5-mini   gpt-5-nano (cheap)\nassertions passed     18/18        13/18      <- less reliable\nParquet eval          3/3 runs     0/3 runs\nasof join eval        3/3 runs     2/3 runs\ntotal cost            $0.015       $0.007     <- less than half the cost\n```\n\nThe cheap model saves more than half the cost, but its answers are less reliable. It passed 13 of 18 checks, compared with 18 of 18 for the default model.\n\n## Share the Agent\n\nOnce you are happy with a configuration, you might want to share it with your teammates.\n\nSharing a Python agent usually means your teammates have to download the code, install the dependencies, and run a setup script before they can use it.\n\ndocker-agent makes sharing agents as easy as sharing Docker images. Teammates run it with one command instead of setting up a Python project.\n\nTo push the YAML file to a registry such as Docker Hub (after `docker login`), run:\n\n```\ndocker agent share push embeddings.yaml docker.io/<username>/polars-docs:v1\n```\n\nTeammates can then run the agent by name, without downloading the file first.\n\n```\ndocker agent run docker.io/<username>/polars-docs:v1\n```\n\nTo get a local copy of the YAML file instead, use `docker agent share pull`.\n\nNote that only the YAML file is shared. Any files it points to, such as docs folders or prompt files, must exist in the folder where you run the agent.\n\n## Final Thoughts\n\nWhat I like about docker-agent is that it takes so little code to get a working agent. An LLM can help you write a Python agent, but you still have to maintain that code, and more code means more places for mistakes.\n\nKeeping everything in one YAML file also makes agents easy to change and easy to read, even for people who do not write Python every day. And with Docker-style sharing, your teammates can try your agent with one command.\n\nBut docker-agent is not the right fit for everything. If you need to control each step the agent takes, with custom branches or retries, a code-first framework like [LangGraph](https://codecut.ai/building-multi-agent-ai-langgraph-tutorial/) is a better fit.\n\nThe same principle of keeping configuration separate from code is a core chapter in [Production-Ready Data Science](https://codecut.ai/production-ready-data-science/).\n\n## References\n\n- **[docker-agent RAG docs](https://docker.github.io/docker-agent/tools/rag/)** (Docker, 2026): retrieval strategies, the default 0.5 similarity threshold, hybrid search with RRF fusion, and SQLite index storage.\n\n## Related Tutorials\n\n- **[Hydra for Python Configuration](https://codecut.ai/stop-hard-coding-in-a-data-science-project-use-configuration-files-instead/)** : The same “change settings without editing code” idea for data pipelines\n\n### Stay Current with CodeCut\n\nEasy-to-digest articles on Python, AI, and open-source tools. Delivered twice a week.", "url": "https://wpnews.pro/news/writing-ai-agents-in-yaml-instead-of-python", "canonical_source": "https://codecut.ai/build-ai-agents-yaml-docker-agent/", "published_at": "2026-10-28 02:30:09+00:00", "updated_at": "2026-10-28 02:41:31.843402+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "ai-products"], "entities": ["Docker", "docker-agent", "Docker Desktop 4.63", "Polars", "OpenAI", "gpt-5-mini", "text-embedding-3-small", "LangChain"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/writing-ai-agents-in-yaml-instead-of-python", "markdown": "https://wpnews.pro/news/writing-ai-agents-in-yaml-instead-of-python.md", "text": "https://wpnews.pro/news/writing-ai-agents-in-yaml-instead-of-python.txt", "jsonld": "https://wpnews.pro/news/writing-ai-agents-in-yaml-instead-of-python.jsonld"}}