A GoodMem connector for Microsoft Semantic Kernel.
In Python and .NET it implements Semantic Kernel's VectorStoreCollection and VectorStore abstractions, so agents built on Semantic Kernel can store and retrieve memories from a GoodMem server without having to configure your own data processing pipeline. The Java connector does not implement Semantic Kernel's vector store interfaces: it provides its own GoodMemCollection and GoodMemVectorStore classes and a GoodMemPlugin kernel plugin.
GoodMem is a centralized memory API for AI agents and LLMs. The point of GoodMem is so that you can easily and efficiently store and retrieve your data/memories through semantic searching, ai summaries, and context-aware results.
GoodMem stores text memories as semantic embeddings in PostgreSQL (via pgvector) and retrieves them by semantic similarity. Because it runs as a shared service, multiple agents can read and write to the same memory spaces simultaneously.
Embeddings are computed server-side, so this connector never needs an
embedding_generator.
In GoodMem all data is hosted in a "Space", an abstract storage unit in GoodMem. Each Space can be configured with embedders and/or chunking strategies. Each Space holds "Memories".
Memories are stored content with associated metadata that are automatically chunked and embedded for efficient retrieval. All Memories belong to a Space.
Embedders convert your data into a vectorized format. GoodMem supports multiple embedding models & providers.
- installation
- configuration
- run sample files
- create your own integration
Requirements: Python 3.10+ and a running GoodMem server.
pip install goodmem-semantic-kernelTo install from source:
git clone https://github.com/PAIR-Systems-Inc/goodmem-semantic-kernel
cd goodmem-semantic-kernel
pip install -e .sudo apt install dotnet-sdk-8.0Build the connector from source:
dotnet build dotnet/GoodMem.SemanticKernel/GoodMem.SemanticKernel.csprojRequirements: JDK 17+ (JDK 21 recommended) and Maven 3.6.3+ (the floor of the compiler and surefire plugins the build uses).
Install JDK 21 via SDKMAN (recommended):
sdk list java | grep -- '-tem' # identifiers change; pick the current 21.x one
sdk install java 21.0.12+1.1-temOr via apt:
sudo apt install openjdk-21-jdkBuild and install the connector into your local Maven repository:
mvn install -f java/pom.xml -DskipTestsAll settings are read from environment variables with the GOODMEM_ prefix, or passed directly via GoodMemSettings (Python), GoodMemOptions (.NET) or GoodMemOptions.builder() (Java).
export GOODMEM_API_KEY=your_key_here
export GOODMEM_BASE_URL=https://your_goodmem_server:8080
export GOODMEM_VERIFY_SSL=true_or_false
export GOODMEM_EMBEDDER_ID=your_embedder_uuid| Variable | Read by | Required | Default | Description |
|---|---|---|---|---|
GOODMEM_API_KEY |
all | Yes | — | API key for the GoodMem server |
GOODMEM_BASE_URL |
all | No | http://localhost:8080 |
GoodMem server base URL |
GOODMEM_EMBEDDER_ID |
all | Python: yes, to create a collection | — | UUID of the embedder a new space is indexed with; the choice is permanent for a space. Python will not choose one for you. .NET and Java use the first embedder the server lists when this is unset, so set it there too |
GOODMEM_VERIFY_SSL |
all | No | true |
Set to false for self-signed certs |
GOODMEM_RERANKER_ID |
Python | No | — | UUID of a reranker to apply to searches |
GOODMEM_TIMEOUT |
Python | No | 30 |
Per-request timeout in seconds (.NET and Java use a fixed 30 s) |
GOODMEM_WAIT_FOR_INDEXING |
Python | No | true |
Wait for each written memory to finish indexing, so a search straight after a write can find it (.NET and Java do not wait) |
GOODMEM_INDEXING_TIMEOUT |
Python | No | 60 |
How long that wait lasts |
cd samples/python
# Option A — agent with memory tool (also requires OPENAI_API_KEY)
export OPENAI_API_KEY=your_openai_key_here
python example_agent.py
# Option B — single collection
python example_single_collection.py
# Option C — store with multiple collections
python example_store.pyThe samples need the four variables in Configuration, GOODMEM_EMBEDDER_ID included, or creating their space fails. If a sample still fails, run it inside a virtual environment:
python3 -m venv venv
source venv/bin/activatecd samples/dotnet/ExampleAgent
dotnet runEach sample lists its required environment variables at the top of Program.cs.
Build the connector once before running any sample:
mvn install -f java/pom.xml -DskipTestsThen run any sample:
cd samples/java/ExampleAgent
mvn compile exec:javaEach sample lists its required environment variables in the file header.
These are the same commands CI runs.
# Python: 206 offline tests. 44 drive the real SDK over a mock HTTP transport,
# using event shapes captured from a live server. 150 drive the whole stack
# over TCP to a local server that records every request, to check that no id
# reaches a request path unless it is a UUID. The last 12 run this README's
# Python snippets against a local stand-in server and check its facts.
pip install -e ".[dev]"
ruff check python/ && ruff format --check python/
mypy
pytest python/tests -q
pip install build && python -m build
# CI then fails the job if an API key is committed (the "No API key in the
# tree" step of .github/workflows/ci.yml).
# Python: 13 more live tests run when a server is configured. Without these
# variables they skip, which is also how we check no credential is baked in.
GOODMEM_BASE_URL=https://localhost:8080 \
GOODMEM_API_KEY=your_key_here \
GOODMEM_EMBEDDER_ID=your_embedder_uuid \
GOODMEM_VERIFY_SSL=false \
pytest python/tests -q
# .NET: 185 offline tests (3 integration tests skip without GOODMEM_API_KEY).
# 18 check how a search reports the server's statuses, on retrieval streams
# captured from a live server, through the real HTTP client and a mock handler.
dotnet build dotnet/GoodMem.SemanticKernel/GoodMem.SemanticKernel.csproj --configuration Release
dotnet test dotnet/GoodMem.SemanticKernel.Tests/GoodMem.SemanticKernel.Tests.csproj --configuration Release
# Java: 162 tests, against WireMock and a local recording server. 19 check
# how a search reports the server's statuses, on captured retrieval streams.
mvn -B -f java/pom.xml testfrom dataclasses import dataclass
from typing import Annotated
from semantic_kernel.data.vector import VectorStoreField, vectorstoremodel
@vectorstoremodel
@dataclass
class Note:
id: Annotated[str | None, VectorStoreField("key")] = None
content: Annotated[str, VectorStoreField("data", type="str")] = ""
source: Annotated[str | None, VectorStoreField("data")] = None- Exactly one
"key"field (the memory ID —Nonelets the server generate a UUID; any other value must be a UUID). - One
"data"field namedcontentbecomes the embedded text (originalContentin GoodMem). - All other
"data"fields are stored as metadata and returned on search results. "vector"fields are accepted for interface compatibility but ignored — GoodMem embeds server-side.
We have three example patterns provided in the samples directory. We recommend option A, but choose what works for you.
Option A (samples/python/example_agent.py) is the recommended pattern for production agents since the LLM decides when to call memory and what to search for, rather than the application hardcoding those decisions.
import asyncio
from semantic_kernel.agents import AgentThread, ChatCompletionAgent
from semantic_kernel.connectors.ai import FunctionChoiceBehavior
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
from semantic_kernel.functions import KernelPlugin
from goodmem_semantic_kernel import GoodMemCollection
async def main():
async with GoodMemCollection(record_type=Note, collection_name="agent-memory") as coll:
await coll.ensure_collection_exists()
await coll.upsert([ # seed your memories
Note(content="The Golden Gate Bridge is in San Francisco.", source="geography"),
])
memory_plugin = KernelPlugin(
name="memory",
functions=[
coll.create_search_function(
function_name="recall",
description="Search long-term memory for relevant facts.",
string_mapper=lambda r: r.record.content,
)
],
)
agent = ChatCompletionAgent(
name="MemoryAgent",
service=OpenAIChatCompletion(ai_model_id="gpt-4o-mini"), # reads OPENAI_API_KEY
instructions="Always search memory before answering factual questions.",
function_choice_behavior=FunctionChoiceBehavior.Auto(),
plugins=[memory_plugin],
)
thread: AgentThread | None = None
result = await agent.get_response(messages="Where is the Golden Gate Bridge?", thread=thread)
print(result.content)
asyncio.run(main())see example_single_collection.py
see example_store.py
-
Ids must be UUIDs. Record keys are GoodMem memory ids, and ids go into request URLs, so a key such as
../spaces/<id>could otherwise send a delete to a different resource. The connector refuses any key, and any configured embedder or reranker id, that is not a canonical UUID, before it sends anything. It also never puts a space or memory id that the server returned into a URL unless that id is a UUID. .NET raisesArgumentExceptionand Java raisesIllegalArgumentException. To let the server assign a key, passNone(Python) ornull(.NET, Java). In Python the exception depends on what was refused:- A key passed to
get,upsertordeleteraisesVectorStoreOperationException. The connector raisesValueError, and Semantic Kernel wraps it. GOODMEM_RERANKER_IDraisesVectorSearchExecutionExceptionfromsearch. That is a subclass ofVectorStoreOperationException.GOODMEM_EMBEDDER_IDraisesVectorStoreInitializationExceptionfromensure_collection_exists. That is not aVectorStoreOperationException, soexcept VectorStoreOperationExceptiondoes not catch it. Semantic Kernel wraps it inVectorStoreOperationExceptionwhenupserthits it first, and inVectorSearchExecutionExceptionwhensearchdoes.- A space id the server listed that is not a UUID makes
ensure_collection_deletedraiseVectorStoreOperationExceptionand delete nothing.GoodMemStore.ensure_collection_deleted(name)raises it too, where Semantic Kernel's default would have swallowed it. - A memory id that is not a UUID in the server's answer to a create makes
upsertraiseVectorStoreOperationException. Its__cause__is aGoodMemUpsertError: that record was written, andwritten_keyslists the records written before it.
- A key passed to
-
No local embedding. Never pass an
embedding_generator— GoodMem embeds content server-side. The parameter is accepted for interface compatibility and silently ignored. -
Upsert semantics. GoodMem memories are immutable — there is no update endpoint — so upserting a record that already exists deletes the old memory and creates a new one. The connector reads the current version before the delete and writes it back if the create fails, then raises
GoodMemUpsertError(Python) /GoodMemUpsertException(.NET, Java) saying whether the restore succeeded. Semantic Kernel wraps what a collection raises, so in Python the detail is on__cause__:from semantic_kernel.exceptions import VectorStoreOperationException try: await collection.upsert(note) except VectorStoreOperationException as exc: detail = exc.__cause__ # GoodMemUpsertError detail.restored # True when the old version is back detail.lost_key # set only if it could not be restored detail.written_keys # records written before the failure
-
contentis write-only in GoodMem. The server does not returnoriginalContentin search responses. Retrieved text comes fromchunkText(a chunk of the original), which the connector maps back to yourcontentfield transparently. -
Score convention. A GoodMem vector
relevanceScoreis a raw pgvector value where lower means more similar, so the connector negates it and Semantic Kernel's higher-is-better convention holds. A reranker score (Python only; .NET and Java send no reranker) is already higher-is-better and is passed through unchanged — reranker ranges are provider-dependent (Voyage rerank-2.5 returns roughly0.27..0.93, Jina v3-0.14..0.43), so do not assume 0–1 when choosing a threshold.The connector decides the kind of score from what the server did, not from
GOODMEM_RERANKER_ID. When the reranker cannot run, the server reportsRERANKING_FAILED(and, when the id names no reranker, aNOT_FOUNDnaming it) and still returns its vector hits. The connector scores those as vector hits, so the best match still scores highest.KernelSearchResults.metadatahasgoodmem_partialset toTrueand the codes ingoodmem_statuses, and no hit is dropped. A threshold chosen for reranker scores does not fit these scores, so checkgoodmem_statusesfor either code before applying one. -
A search the server reported a problem with says so (.NET, Java). GoodMem sends
statusevents in a search's response stream, for exampleNOT_FOUNDandRERANKING_FAILEDwhen a reranker does not exist, orEMBEDDER_FAILED. The connectors follow the retrieval status contract every GoodMem integration follows:FEATURE_DISABLEDandLLM_CAPABILITY_INFERREDare notices about optional features the search did not ask for. They are ignored, by their code alone.- Any other status marks the search partial. The results the server did return are always kept, and a reported problem never throws, even when nothing came back.
- A code the connector does not recognise is reported as
UNKNOWN, with the server's own code kept inOriginalCode(.NET) /originalCode()(Java). - A line of the response stream that cannot be parsed is reported as
MALFORMED_STREAM, and the lines around it are still read.
SearchWithStatusAsync(.NET) returns aGoodMemSearchResults<TRecord>withResults,PartialandStatuses;searchWithStatus(Java) returns aGoodMemCollection.SearchResults<T>withresults(),partial()andstatuses(). Each status is aGoodMemRetrievalStatus:var search = await collection.SearchWithStatusAsync("european capitals", top: 3); if (search.Partial) foreach (var status in search.Statuses) Console.WriteLine(status); // e.g. "RERANKING_FAILED: Failed to create reranker client: ..."
var search = collection.searchWithStatus("european capitals", 3).block(); if (search.partial()) search.statuses().forEach(System.out::println); // code, message and details, one per line
Semantic Kernel's
SearchAsync(.NET), the Javasearchand the Java plugin'srecallhave nowhere to put the flag, so they log a warning that names the statuses whenever the search was partial.SearchWithStatusAsyncandsearchWithStatuslog one only when a partial search returned nothing. .NET logs throughGoodMemOptions.LoggerFactory, or to standard error when it is not set (setNullLoggerFactory.Instanceto silence it). Java logs throughSystem.Loggerunderai.goodmem.semantickernel.GoodMemCollection, whichjava.util.loggingprints to standard error unless the application routes it elsewhere. -
Filters (Python).
search(filter=...)is translated to a GoodMem filter expression and evaluated server-side. For a record type withtagandyeardata fields:await collection.search("quarterly", filter=lambda n: n.tag == "finance" and n.year > 2000)
==,!=,<,<=,>,>=,in,not in,and,orandnotare supported. Values are quoted and cast for you — a value containing an apostrophe is a value, not syntax — and a boolean is compared with aBOOLEANcast, because comparing one as text is accepted by the server and matches nothing. Two limits come from Semantic Kernel itself, which re-parses the lambda's own source: the value must be a literal rather than a variable, and the call has to fit on one line. Only metadata fields can be filtered; thecontentfield is the embedded body, not metadata. The .NET connector raisesNotSupportedExceptionfor a filter, and the Javasearchtakes none. -
Pre-computed vectors not supported. Passing
vector=tosearch()raisesVectorStoreOperationNotSupportedException(Python); a non-string search value raisesNotSupportedException(.NET). Pass text only.
goodmem-semantic-kernel/ ← repo root
├── python/
│ ├── goodmem_semantic_kernel/ ← importable Python package
│ │ ├── __init__.py # Public exports: GoodMemCollection, GoodMemStore, GoodMemSettings
│ │ ├── _connection.py # Owns (or borrows) the official goodmem SDK client
│ │ ├── _ids.py # Refuses any id that is not a UUID before it is sent
│ │ ├── _results.py # Retrieval statuses, chunk→memory join, score direction
│ │ ├── _typing.py # Protocols for the SDK surface this package calls
│ │ ├── collection.py # VectorStoreCollection + VectorSearch implementation
│ │ ├── filters.py # Builds GoodMem filter expressions safely
│ │ ├── settings.py # GoodMemSettings (Pydantic, reads GOODMEM_* env vars)
│ │ └── store.py # VectorStore implementation
│ └── tests/ # support.py + test_regressions.py + test_id_validation.py + test_readme.py + test_e2e.py
├── dotnet/
│ └── GoodMem.SemanticKernel/ ← .NET connector library
│ └── GoodMem.SemanticKernel.Tests/
├── java/
│ ├── pom.xml ← parent Maven POM
│ └── goodmem-semantic-kernel/ ← Java connector library
│ └── src/main/java/ai/goodmem/semantickernel/
│ ├── GoodMemCollection.java # Typed CRUD + semantic search (Reactive)
│ ├── GoodMemVectorStore.java # Factory for multiple collections
│ ├── GoodMemPlugin.java # SK KernelPlugin: save + recall functions
│ ├── GoodMemSchema.java # Reflection engine for @GoodMemKey/@GoodMemData
│ ├── GoodMemKey.java # Annotation: marks the memory ID field
│ ├── GoodMemData.java # Annotation: marks content/metadata fields
│ ├── GoodMemClient.java # Async HTTP client (GoodMem REST API)
│ ├── GoodMemOptions.java # Configuration (reads GOODMEM_* env vars)
│ ├── GoodMemIds.java # Refuses any id that is not a UUID before it is sent
│ ├── GoodMemRetrievalStatus.java # A problem the server reported during a search
│ ├── RetrievalStatuses.java # Sorts a search's status events (the status contract)
│ ├── GoodMemUpsertException.java # A failed update: restored or lost key
│ └── GoodMemException.java # Runtime exception wrapper
├── samples/
│ ├── python/ ← Runnable Python samples
│ ├── dotnet/ ← Runnable .NET samples
│ └── java/ ← Runnable Java samples
└── pyproject.toml
The core class. Implements VectorStoreCollection[str, TModel] and VectorSearch[str, TModel].
GoodMemCollection(
record_type=MyModel,
collection_name="my-space", # maps to a GoodMem Space
settings=GoodMemSettings(), # optional; reads GOODMEM_* env vars by default
client=None, # optional; inject a pre-built goodmem.AsyncGoodmem
)| Method | Description |
|---|---|
ensure_collection_exists() |
Create the GoodMem space if it doesn't exist |
ensure_collection_deleted() |
Delete the space and all its memories |
collection_exists() |
Return True if the space exists |
upsert(records) |
Write one or a list of records; returns the memory ID(s) |
get(key=...) / get(keys=[...]) |
Fetch memories by ID (each a UUID) |
delete(keys=[...]) |
Delete memories by ID (each a UUID) |
search(query, top=3) |
Semantic search; returns KernelSearchResults |
create_search_function(...) |
Wrap search as a KernelFunction for use in agent plugins |
Factory for collections. All collections from the same store share one HTTP connection.
GoodMemStore(settings=GoodMemSettings())| Method | Description |
|---|---|
get_collection(record_type, collection_name=...) |
Return a GoodMemCollection |
list_collection_names() |
List all GoodMem spaces visible to this API key |
ensure_collection_deleted(collection_name) |
Delete the named space, if it exists. A refused delete raises instead of passing silently |
Pydantic settings class; reads GOODMEM_* environment variables.
GoodMemSettings(
base_url="https://localhost:8080",
api_key="your_key_here",
embedder_id="your_embedder_uuid", # required to create a space
reranker_id=None,
verify_ssl=True,
timeout=30.0,
wait_for_indexing=True,
indexing_timeout=60.0,
)