Metadata-Version: 2.5
Name: goodmem-crewai
Version: 0.4.0
Summary: GoodMem knowledge storage and RAG tools for CrewAI agents
Project-URL: Homepage, https://github.com/PAIR-Systems-Inc/goodmem-crewai
Project-URL: Source, https://github.com/PAIR-Systems-Inc/goodmem-crewai
Project-URL: Documentation, https://docs.goodmem.ai/docs/integrations/agent-frameworks/crewai/
Project-URL: Issues, https://github.com/PAIR-Systems-Inc/goodmem-crewai/issues
Project-URL: Changelog, https://github.com/PAIR-Systems-Inc/goodmem-crewai/blob/main/CHANGELOG.md
Author: PAIR Systems Inc
License: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: agents,crewai,goodmem,memory,rag,tools
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: <3.14,>=3.11
Requires-Dist: crewai>=1.15.9
Requires-Dist: goodmem>=0.1.34
Provides-Extra: dev
Requires-Dist: httpx; extra == 'dev'
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: python-dotenv>=1; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# goodmem-crewai

GoodMem knowledge storage and RAG tools for [CrewAI](https://crewai.com).

[GoodMem](https://goodmem.ai) is a self-hostable RAG service that handles
embedding, chunking, storage and retrieval on the server. This package plugs it
into CrewAI two ways: as a knowledge backend for CrewAI's own `Knowledge`, and
as tools an agent can call directly.

## Install

```bash
pip install goodmem-crewai
```

Python 3.11–3.13, CrewAI 1.15.9+. Runtime dependencies are `crewai` and the
official `goodmem` SDK (0.1.34+).

Set `GOODMEM_BASE_URL` and `GOODMEM_API_KEY`, or pass `base_url`/`api_key`, or
inject a configured `Goodmem` client.

## As a knowledge backend

```python
from crewai import Agent, Crew, Knowledge, Task
from goodmem_crewai import GoodMemKnowledgeStorage

storage = GoodMemKnowledgeStorage(space_id="<space-id>", reranker_id="<reranker-id>")
knowledge = Knowledge(collection_name="handbook", sources=[], storage=storage)

storage.save(["Refunds over $500 need a manager's approval."])

agent = Agent(
    role="Support Lead",
    goal="Answer policy questions from the handbook.",
    backstory="You cite the passage you relied on.",
    knowledge=knowledge,
)
```

## As an agent tool

The search tool takes only a query from the model. Which spaces it searches,
how many results it returns, whether it reranks, whether an LLM answers and
any metadata filter are set by you, so a model cannot redirect the search
mid-run.

```python
from crewai import Agent
from goodmem_crewai import GoodMemSearchTool

search = GoodMemSearchTool(
    space_ids=["<space-id>"],
    k=5,
    reranker_id="<reranker-id>",          # optional
    llm_id="<llm-id>",                    # optional: an answer from an LLM
    metadata_filter={"category": "policy"},  # optional, escaped for you
)

agent = Agent(role="Researcher", goal="Answer from the knowledge base",
              backstory="You cite sources.", tools=[search])
```

`metadata_filter` values are compared as their own JSON type: a `str` as text,
a `bool` as a boolean and an `int`/`float` as a number, so `{"archived": True}`
matches a stored `true`. Any other value (`None`, a list, a dict) is refused
with `ValueError`; use `filter` to pass an expression directly.

### An answer from an LLM (opt-in)

Set `llm_id` to the id of an LLM registered in GoodMem and the server runs it
over the passages it retrieved. The tool's output then carries the answer as
`abstract_reply` (`{"text": "...", "relevance_score": ..., "result_set_id":
"..."}`) next to `results`, which are returned as usual. It is off unless you
set it, and it is a field on the tool, not an argument: the model cannot turn
it on or pick a different LLM. Like every id it must be a UUID; anything else
is refused with `INVALID_INPUT` before a request is made. An LLM does not
rerank, so `score` and `score_kind` are the same with or without one.

If the LLM fails — its provider refuses the call, or no LLM has that id — the
passages are still returned, with `partial: true`, the server's `statuses`
(`SUMMARIZATION_FAILED`, plus `NOT_FOUND` for an id that does not exist) and
no `abstract_reply`. Nothing is raised.

`GoodMemKnowledgeStorage` has no `llm_id`. CrewAI puts only each result's
`content` into the agent's prompt, and runs a knowledge search for every
task, so an answer generated there would be paid for each time and then
discarded. Give the agent `GoodMemSearchTool` with `llm_id` instead.

### Partial results

Results carry `partial` and `statuses`. If part of a search failed — a reranker
was unavailable, one space was unreachable — you get the usable passages *and*
the fact that they are incomplete. A search that produced nothing usable
returns empty results with `partial: true` and the statuses, so the model can
tell a failed search from a miss; it is never raised. `GoodMemKnowledgeStorage`
returns a bare list, so in that case it emits a warning and a log line instead.

## Tools

| Tool | Purpose |
| --- | --- |
| `GoodMemSearchTool` | Semantic search; the model passes only a query |
| `GoodMemCreateMemoryTool` | Store text; waits for indexing by default |
| `GoodMemUploadFileTool` | Store a file from a configured `upload_dir` (opt-in) |
| `GoodMemGetMemoryTool` | Fetch a memory with readable content |
| `GoodMemListMemoriesTool` / `GoodMemDeleteMemoryTool` | Manage memories |
| `GoodMemListSpacesTool` / `GoodMemGetSpaceTool` | Find spaces |
| `GoodMemCreateSpaceTool` / `GoodMemUpdateSpaceTool` / `GoodMemDeleteSpaceTool` | Manage spaces |
| `GoodMemListEmbeddersTool` / `GoodMemListRerankersTool` | Discover model IDs |

Space, memory and file tools carry the authority of the configured API key.
Give them only to crews that need it.

Every id, whether a model passes it or you configure it, must be a UUID;
anything else is refused before a request is made (a tool returns a
`ToolFailure` with reason `INVALID_INPUT`, `GoodMemKnowledgeStorage` and
`wait_for_memories` raise `ValueError`), because the SDK puts ids into the URL
path unescaped and an id such as `../spaces/<id>` would otherwise send the call
to a different resource.

## Waiting for indexing

Searching is not a way to wait for a write. `GoodMemCreateMemoryTool` waits for
its own memory by default; use `wait_for_memories(ids)` to wait on specific IDs.

## Scores

CrewAI's `SearchResult.score` is documented as higher-is-better. GoodMem's
vector score is a negative inner product — the best match is the *lowest*
number (a live capture ranked `-0.6154` above `-0.3873`) — so it is negated to
fit that convention; a reranker score already runs the right way and is passed
through. The untouched server value is kept as `metadata["raw_score"]`, and
`metadata["score_kind"]` (`"vector"` or `"reranker"`) names the scale.

Neither scale is 0–1. `score_threshold` is therefore applied only when a
reranker produced the scores; without one it is ignored with a warning.
`score_kind` follows what the server did, not what was configured: if the
reranker fails (`RERANKING_FAILED`, or `NOT_FOUND` for the reranker), the
server still returns the vector search's hits, and they are kept as `"vector"`
results — negated, not thresholded, and flagged partial with the statuses.

Even with a reranker, the scale is **model-dependent**: on the same documents
Voyage `rerank-2.5` scored `0.27..0.93` and Jina `jina-reranker-v3` scored
`-0.14..0.43`. CrewAI's default `score_threshold=0.6` keeps the top results on
the first and removes everything on the second, so if a threshold drops every
result the storage warns and names the observed range rather than returning a
silent empty list. Calibrate the threshold for the reranker you use.

## Development

```bash
uv sync --extra dev
uv run ruff check . && uv run ruff format --check . && uv run mypy src
uv run pytest -m "not e2e"    # offline: the SDK over a mock transport and a local HTTP server
GOODMEM_BASE_URL=… GOODMEM_API_KEY=… GOODMEM_EMBEDDER_ID=… GOODMEM_LLM_ID=… GOODMEM_VERIFY_SSL=false uv run pytest -m e2e
```

`tests/test_readme.py` runs every Python snippet in this README, as written,
against a local mock server; CI runs it as its own step.

`GOODMEM_VERIFY_SSL=false` is for a local server with a self-signed certificate.
`GOODMEM_LLM_ID` is optional; without it the live LLM-answer test is skipped.

Apache-2.0.
