CrewAI
CrewAI orchestrates a crew of role-playing agents that call tools to get
work done. Korely is the memory underneath: typed bi-temporal facts that
resolve their own contradictions, point-in-time recall, and semantic vector
search over everything the user told you, all behind one endpoint. The
integration is a handful of custom tools that wrap Korely
SDK calls. You attach them to an Agent, and from that point the
model decides when to recall, when to save, and when to search.
The tool that carries the moat is recall. Korely's
get_context does not hand back a pile of rows for the model to
sift, it assembles the user's active typed facts (the
bi-temporal (subject, predicate, object) triples Korely keeps
current through contradiction resolution) plus the most relevant memories
into one ready-to-read block. Give a CrewAI agent that single tool and it
gets settled knowledge, "Luca upgraded to Pro", "prefers async standups",
instead of re-reading raw snippets every turn.
What you get
Each tool calls one Korely method under the hood. The model receives clean output and decides when to invoke memory. On the read side the primary path is recall, fact-assembled context over the user's active typed facts, with semantic search as the targeted-lookup fallback. On the write side: save a memory. No schema writing, no glue code beyond the tool bodies shown below.
Install
You need CrewAI and the Korely Python package:
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/agents, copy the kor_live_ key
from the dashboard, and export it:
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.
import osfrom typing import Type
from crewai.tools import BaseToolfrom pydantic import BaseModel, Field
from korely_memory import Korely
# Reads KORELY_API_KEY from the environment; EU-hosted on every plan.korely = Korely(api_key=os.environ["KORELY_API_KEY"], region="eu")
# ---- recall: the moat tool. 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. Raw search
returns matching memory snippets that the model still has to read and
reconcile. get_context goes further: it assembles the user's
active typed facts, the bi-temporal triples Korely extracts and keeps
current through contradiction resolution, into a compact,
ready-to-prompt block. The returned Context carries
.context (the string you hand to the model), plus
.tokens and .sources for inspection. Search
stays useful for "find the exact memory that mentioned X"; context is
what you reach for when you want the agent to simply know the
user.
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 on its own,
nudge it through the agent's goal and backstory:
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="What did the user decide about the lease renewal? {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": "Recall before answering."})print(result)
Run it and the agent reaches for Recall Memory on its own,
which calls korely.get_context under the hood and reasons over
the assembled block with its own model:
$ python crew.py
→ tool call Recall Memory(query="lease renewal decision")
← korely.get_context returns an assembled block:
FACTS
- user → renewed → apartment lease (valid from 2026-05-28)
- landlord (Anna) → set rent → 1,236 EUR/month (from August)
MEMORIES
- 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 starting in August,
and she asked for the signed copy by post.
What the tool hands back from get_context is assembled, not
generated: the active typed facts and the memories that back them. No
generative model runs on Korely's read path; the CrewAI agent's own model
does the reasoning. That is also why read quotas are an order of magnitude
more generous than write quotas.
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:
import os
from crewai.tools import toolfrom korely_memory import Korely
korely = Korely(api_key=os.environ["KORELY_API_KEY"], region="eu")
@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 instances, the same shape an
Agent accepts from a BaseTool subclass.
Multi-tenant crews: scope by user_id
The tools above write into your workspace, which is right for a personal
assistant or an internal ops crew. When you are building a product where
each end user needs their own isolated memory, scope every read and
write with an end-user identifier. Korely's naming for the three scopes:
user_id is the end user (unlimited on every plan, you choose
the string), agent_id is your application, run_id
is one session.
Bind user_id into the tool when you build it, server-side, so
the model can never reach across tenants no matter what the conversation
says:
import osfrom typing import Type
from crewai.tools import BaseToolfrom pydantic import BaseModel, Field
from korely_memory import Korely
korely = Korely(api_key=os.environ["KORELY_API_KEY"], region="eu")
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, server-side. 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")
Each user_id is its own memory space: facts, graph and
retrieval never cross between them. Quotas count memories and queries,
never people, end users are unlimited on every plan.
Facts are extracted asynchronously. korely.add(content) returns immediately with the stored
memory, but Korely extracts the typed (subject, predicate,
object) facts from it a few seconds later, in the background. So a
fact you just wrote may not show up in the very next
get_context or get_facts call within the same
turn, give extraction a moment, then re-read. The raw memory is
searchable right away; the structured facts catch up shortly after.
Troubleshooting
| Symptom | Fix |
|---|---|
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 |
Tool choice is the model's. Put the rule in the agent's
backstory ("always recall before answering") and keep
each tool's description short and action-oriented.
Smaller models need the nudge more than larger ones.
|
get_facts returns nothing for a fresh write |
Fact extraction runs asynchronously after add, so a
just-written memory may not have facts yet. Give it a moment, then
re-read. The raw memory is searchable immediately.
|
Where to go next
- Memory as a tool, the framework-agnostic pattern behind this page, end to end.
- Get context, how the
assembled recall block is built, what
token_budgetcontrols, and what comes back in.sources. - Python SDK reference, the full
add/search/get_context/get_facts/get_profilesurface.
Something not working? Email
[email protected] with your
crewai and korely-memory versions and the
traceback. We read every message.