Korely

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:

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/agents, copy the kor_live_ key from the dashboard, and export it:

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
import os
from typing import Type
from crewai.tools import BaseTool
from 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:

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="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:

crewai crew python crew.py
$ 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 tool
from 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 os
from typing import Type
from crewai.tools import BaseTool
from 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

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 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_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.