Korely

CrewAI

CrewAI runs a crew of role-playing agents that call tools to get work done. This page gives those agents a memory that lasts beyond one run, through the Korely Python SDK. You write a few custom tools, each wrapping one SDK call, and attach them to an Agent. From then on the model decides when to recall, when to save and when to search.

The recall tool uses get_context. It does not hand the model a pile of rows to sift. It returns a context block: the facts that are true now (for example "Luca upgraded to Pro", "prefers async standups") plus the best-matching memories. A fact that a newer one replaced is closed and does not appear.

What the tools do

Each tool calls one Korely method. Recall is the main read: it assembles the current facts. Search finds the exact memory that mentioned something. Save stores a new memory. There is no schema to write and no glue code beyond the tool bodies below.

Install

You need Python 3.10 to 3.13 (CrewAI's supported range), CrewAI and the Korely Python package. The examples were run with crewai 1.15.33 and korely-memory 0.1.28, with a fake model server, so no model provider was called.

Terminal window
pip install crewai korely-memory

The custom-tool primitives, BaseTool and the tool decorator, ship in the core crewai package under crewai.tools. CrewAI's library of pre-built tools (search, scraping and so on) lives in the separate crewai-tools package and is not required here.

You also need a Korely API key. Sign up at korely.ai, copy the kor_live_ key from the dashboard, and export it. CrewAI's own model needs its provider key too (for the default OpenAI model, OPENAI_API_KEY):

Terminal window
export KORELY_API_KEY="kor_live_..."

Define the memory tools

Subclass BaseTool for typed, validated input: each tool declares an args_schema (a Pydantic model), a clear description the agent reads to decide when to use it, and a _run method that does the work. We define three, recall, save and search, each wrapping one Korely call.

korely_tools.py
from typing import Type
from crewai.tools import BaseTool
from pydantic import BaseModel, Field
from korely_memory import Korely
korely = Korely() # reads KORELY_API_KEY from the environment
# ---- recall: the main read. Reach for this first. -------------------------
class RecallInput(BaseModel):
"""Input schema for RecallMemoryTool."""
query: str = Field(..., description="What you want to recall about the user.")
class RecallMemoryTool(BaseTool):
name: str = "Recall Memory"
description: str = (
"Recall settled facts and relevant memories about the user. "
"Returns an assembled context block, not raw rows. "
"Use this before answering anything about the user's past."
)
args_schema: Type[BaseModel] = RecallInput
def _run(self, query: str) -> str:
ctx = korely.get_context(query=query, token_budget=800)
return ctx.context or "No memory yet."
# ---- save: write something worth remembering ------------------------------
class SaveInput(BaseModel):
"""Input schema for SaveMemoryTool."""
content: str = Field(..., description="A durable fact worth remembering.")
class SaveMemoryTool(BaseTool):
name: str = "Save Memory"
description: str = (
"Save a durable fact to memory. Use when the user tells you "
"something worth remembering for later turns or sessions."
)
args_schema: Type[BaseModel] = SaveInput
def _run(self, content: str) -> str:
memory = korely.add(content)
return "Saved as " + memory.id
# ---- search: targeted lookup of an exact memory ---------------------------
class SearchInput(BaseModel):
"""Input schema for SearchMemoryTool."""
query: str = Field(..., description="Keyword-style query, 1 to 5 words.")
class SearchMemoryTool(BaseTool):
name: str = "Search Memory"
description: str = (
"Find the exact memory that mentioned something. "
"Use when recall is not specific enough and you need the raw snippet."
)
args_schema: Type[BaseModel] = SearchInput
def _run(self, query: str) -> str:
hits = korely.search(query, limit=5)
if not hits:
return "No matching memories."
return "\n".join("- " + (hit.snippet or "") for hit in hits)

Why get_context for recall. A search returns matching snippets that the model still has to read and reconcile. get_context goes one step further and returns the current facts, already reconciled, in one block. The returned Context object has .context (the text you hand to the model), .tokens (its size) and .sources (the ids of the facts and memories behind it). Keep search for "find the exact memory that mentioned X".

Attach the tools and run

Instantiate the tools, attach them to an Agent via tools=[...], wrap the work in a Task, and run the Crew. The agent decides when to call each tool. You nudge it through the agent's goal and backstory:

crew.py
from crewai import Agent, Task, Crew, Process
from korely_tools import RecallMemoryTool, SaveMemoryTool, SearchMemoryTool
recall = RecallMemoryTool()
save = SaveMemoryTool()
search = SearchMemoryTool()
assistant = Agent(
role="Personal Assistant",
goal="Answer using what you already know about the user.",
backstory=(
"You have a persistent memory. Always recall before answering "
"questions about the user's past, and save anything durable they tell you."
),
tools=[recall, save, search],
verbose=True,
)
answer = Task(
description="Answer this question about the user: {question}",
expected_output="A short, specific answer grounded in memory.",
agent=assistant,
)
crew = Crew(
agents=[assistant],
tasks=[answer],
process=Process.sequential,
verbose=True,
)
result = crew.kickoff(
inputs={"question": "What did I decide about the lease renewal?"}
)
print(result)

When you run it, the agent calls Recall Memory, which calls korely.get_context, and reasons over the block with its own model. The memories and the answer below are an example, not your output:

crewai crew python crew.py
$ python crew.py

→ tool call Recall Memory(query="lease renewal decision")

← korely.get_context returns an assembled block:

   _The facts below are a compact profile of the user; [...]_

   ## Known facts
   - user renewed apartment lease (since 2026-05-28)
   - Anna confirmed rent increase 3% (since 2026-05-28)

   ## Relevant memories
   _A time in square brackets after a relative expression [...]_
   - [2026-05-28] Renewal deadline is July 1. Anna confirmed a 3% increase,
     1,200 to 1,236 EUR per month. She wants the signed copy by post.

Agent › You decided to renew. The deadline is July 1: Anna
confirmed a 3% increase to 1,236 EUR per month, and she wants the
signed copy by post.

The block that get_context returns is assembled from stored facts and memories, not written by a model. No language model runs when Korely answers a read; the CrewAI agent's own model does the reasoning. Reads are cheap on our side, so every plan allows at least 100 times more queries than writes per month (see the pricing page).

The lightweight @tool decorator

If you do not need a typed args_schema, the tool decorator is the shortest path. The decorated function must have a docstring. CrewAI raises a ValueError without one, and the docstring is what the agent reads to decide when to call it:

from crewai.tools import tool
from korely_memory import Korely
korely = Korely() # reads KORELY_API_KEY from the environment
@tool("Recall Memory")
def recall(query: str) -> str:
"""Recall settled facts and relevant memories about the user."""
return korely.get_context(query=query, token_budget=800).context or "No memory yet."
@tool("Save Memory")
def save(content: str) -> str:
"""Save a durable fact to memory."""
return "Saved as " + korely.add(content).id

Pass the decorated functions straight into Agent(tools=[recall, save]). They are Tool objects, which an Agent accepts like a BaseTool subclass.

One memory per end user

The tools above read and write the whole project. That suits a personal assistant or an internal crew. When each person who uses your product needs a private memory, pass three optional labels on every call. user_id is the end user (unlimited on every plan; you choose the string), agent_id is your application, and run_id is one session. A read without user_id covers every end user of the project.

Bind user_id into the tool when you build it, on your server, so the model cannot reach another person's memory whatever the conversation says:

from typing import Type
from crewai.tools import BaseTool
from pydantic import BaseModel, Field
from korely_memory import Korely
korely = Korely() # reads KORELY_API_KEY from the environment
class RecallInput(BaseModel):
query: str = Field(..., description="What you want to recall about this user.")
class ScopedRecallTool(BaseTool):
name: str = "Recall Memory"
description: str = "Recall everything known about this specific user."
args_schema: Type[BaseModel] = RecallInput
# user_id is bound at construction, on your server. The model never sets it.
user_id: str
def _run(self, query: str) -> str:
ctx = korely.get_context(
query=query,
user_id=self.user_id,
agent_id="support-bot",
token_budget=800,
)
return ctx.context or "No memory yet."
# Per request, bind the end user from the session:
recall = ScopedRecallTool(user_id="customer-4812")

A read that carries a user_id returns only that user's data (we checked this with two users on the Cloud). Quotas count writes and queries, never people.

Facts arrive after the write. korely.add(content) returns at once with the stored memory. The facts are written by extraction, usually about ten seconds later. A fact you just wrote may therefore be missing from the next get_context or get_facts call in the same turn; read again a little later. A search finds the memory itself right away.

Troubleshooting

SymptomFix
ImportError on crewai.tools The canonical import is from crewai.tools import BaseTool and from crewai.tools import tool. Older tutorials use from crewai_tools import BaseTool; prefer crewai.tools from the core package. Do not use from crewai import tool, the tool decorator is not a top-level export.
ValueError: Function must have a docstring A @tool-decorated function needs a docstring; CrewAI uses it as the tool description. Add one line describing what the tool does.
401 Unauthorized from api.korely.ai Key missing or revoked. Check that KORELY_API_KEY is set in the environment and starts with kor_live_. You can also pass it explicitly: Korely(api_key="kor_live_...").
The agent never calls the memory tools The model chooses its tools. Put the rule in the agent's backstory ("always recall before answering") and keep each tool's description short and about the action. Smaller models need the nudge more than larger ones.
get_facts returns nothing right after a write The facts are written about ten seconds after add, so a memory you just wrote may not have facts yet. Read again a little later. A search finds the memory itself immediately.

Where to go next

  • Memory as a tool: the pattern behind this page, for any framework, end to end.
  • Get context: how the recall block is built, what token_budget controls, and what comes back in .sources.
  • Python SDK reference: the full add / search / get_context / get_facts / get_profile surface.

Something not working? Email [email protected] with your crewai and korely-memory versions and the traceback. We read every message.