CrewAICrewAI 1.15 · Python 3.10 to 3.13
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
35 small wins to finish your pathNext lesson →

Integrations: from stand-in to production

Integrations are the real backends that take the place of the keyless stand-ins this course ran on, each a drop-in for the same call so the crew code does not change.

Last updated: 28 Sep, 2026 · CrewAI 1.15

Every lesson stayed runnable with no key by using a stand-in: a model you wrote, a word-hash embedder, a local store, a server started as a subprocess, a listener that printed events. Production swaps each one for a real backend. This lesson maps them, so a reader leaves knowing what to run for real, not only the stand-in.

What you used against a real one

Each row is the same argument with a different value. The left is what ran here; the middle is the production version; the right is where it comes from.

What you usedA real oneThe package or call
ShopLLM, a BaseLLM you wroteA hosted modelLLM(model="gpt-4o-mini"); other providers via crewai[...] extras
word_hash embedderA hosted or local embedderan embedder spec, e.g. {"provider": "openai"} or {"provider": "ollama"}
Memory on a local folderA shared vector storeMemory(storage="qdrant-edge") for Qdrant
MCPServerStdioA remote MCP serverMCPServerHTTP(url="https://...")
The event listener from watchingA tracing backendCrewAI tracing, or an observability vendor

The model is a true drop-in

The stand-in subclassed BaseLLM, and CrewAI's LLM subclasses BaseLLM too. Because both are the same kind of object, swapping one for the other is one assignment. This line needs a key, so it does not run here.

python
from crewai import LLM

clerk.llm = LLM(model="gpt-4o-mini")  # needs OPENAI_API_KEY

The store, from a folder to a service

Memory writes to a local LanceDB folder by default. Point storage at a path to keep runs together, or at "qdrant-edge" to use Qdrant. The remember and recall calls are the same either way, which the run below shows: one Memory object writes to the folder, and a second one reopens the same folder and reads it back.

Project files used on this pageThis lesson builds on a project from earlier lessons. The code below imports these files. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep them in the same folder.
View the code here
embed.py
import hashlib
import math


def word_hash(texts):
    vectors = []
    for text in texts:
        vector = [0.0] * 64
        for word in text.lower().split():
            word = word.strip(".,?!")
            vector[int(hashlib.md5(word.encode()).hexdigest(), 16) % 64] += 1.0
        length = math.sqrt(sum(x * x for x in vector)) or 1.0
        vectors.append([x / length for x in vector])
    return vectors
shop_llm.py
import json
import os
import re

from crewai import BaseLLM

os.environ["OTEL_SDK_DISABLED"] = "true"
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"
os.environ["CREWAI_TRACING_ENABLED"] = "false"
os.environ["CREWAI_DISABLE_VERSION_CHECK"] = "true"


class ShopLLM(BaseLLM):
    script: list = []

    def supports_function_calling(self):
        return True

    def call(self, messages, tools=None, **kwargs):
        if isinstance(messages, str):
            messages = [{"role": "user", "content": messages}]
        if self.script:
            return self.script.pop(0)
        return self.decide(messages, tools or [])

    def decide(self, messages, tools):
        last = messages[-1]
        if last["role"] == "tool":
            return last["content"]
        text = last["content"]
        orders = re.findall(r"\b[A-Z]\d+\b", text)
        want_refund = "refund" in text.lower()
        chosen = None
        for t in tools:
            fn = t["function"]
            label = (fn["name"] + " " + (fn.get("description") or "")).lower()
            is_refund = "refund" in label
            if want_refund and is_refund:
                chosen = fn["name"]
                break
            if not want_refund and not is_refund and ("look up" in label or "status" in label):
                chosen = fn["name"]
                break
        if orders and chosen:
            args = json.dumps({"order_id": orders[0]})
            return [{"id": f"call_{orders[0]}", "type": "function",
                     "function": {"name": chosen, "arguments": args}}]
        if "working with:" in text:
            context = text.split("working with:")[1].strip().split("\n\n")[0]
            return f"Dear customer, {context}"
        if orders:
            return f"I have no way to look up {orders[0]} yet."
        return "Hello. Which order is this about?"

A store that outlives the process

Examplepersisted_memory.py
import os
os.environ["OTEL_SDK_DISABLED"] = "true"
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"
from crewai import Memory
from crewai.events import crewai_event_bus
from embed import word_hash
from shop_llm import ShopLLM

one = Memory(llm=ShopLLM(model="shop"), embedder=word_hash, storage="./deskmem")
with crewai_event_bus.scoped_handlers():
    one.remember("Asha prefers email, not phone calls.",
                 scope="/customer/asha", categories=["preference"], importance=0.8)

two = Memory(llm=ShopLLM(model="shop"), embedder=word_hash, storage="./deskmem")
for match in two.recall("How does Asha want to be contacted?", depth="shallow", limit=1):
    print(match.record.content)

Reading the round trip

  • The second Memory read the first one's fact from the same folder, so nothing was held only in the first process.
  • The API did not change. Only storage moved from the default to a path; remember and recall are identical.
  • The same swap reaches Qdrant with storage="qdrant-edge", which is how a desk shares memory across machines.

Stand-in against production, by concern

ConcernThe stand-inProduction
ModelScripted, no keyHosted, keyed, billed
EmbedderWord matchingTrained on meaning
StoreA local folderA shared service
ToolsA subprocessA deployed server
TracingPrinted eventsA tracing dashboard

When to swap each piece

  • Swap the model and embedder first, since they decide answer quality.
  • Swap the store when more than one process or machine must share memory.
  • Keep the stand-ins in your test suite, where a stable output is worth more than a real one.
Watch out
Each real backend needs a key or a running service, and a hosted model changes its wording every run. Swap one piece at a time and rerun your guardrail checks after each, so a failing swap is easy to place.
Try it yourself
  • Change storage to a new folder name and watch a fresh store start empty.
  • Remember two facts in the first process and recall them in the second.
  • Read the docs' embedder page and write the spec for a provider you have a key for.

Every expert started right here.