Skip to main content

goodmem-semantic-kernel

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.

What is GoodMem?

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.

Conceptual Overview

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.


Quickstart

  1. installation
  2. configuration
  3. run sample files
  4. create your own integration

Installation

Requirements: Python 3.10+ and a running GoodMem server.

pip install goodmem-semantic-kernel

To install from source:

git clone https://github.com/PAIR-Systems-Inc/goodmem-semantic-kernel
cd goodmem-semantic-kernel
pip install -e .

.NET (debian/ubuntu)

sudo apt install dotnet-sdk-8.0

Build the connector from source:

dotnet build dotnet/GoodMem.SemanticKernel/GoodMem.SemanticKernel.csproj

Java (debian/ubuntu)

Requirements: 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-tem

Or via apt:

sudo apt install openjdk-21-jdk

Build and install the connector into your local Maven repository:

mvn install -f java/pom.xml -DskipTests

Configuration

All 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

Running the samples

Python

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

The 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/activate

.NET

cd samples/dotnet/ExampleAgent
dotnet run

Each sample lists its required environment variables at the top of Program.cs.

Java

Build the connector once before running any sample:

mvn install -f java/pom.xml -DskipTests

Then run any sample:

cd samples/java/ExampleAgent
mvn compile exec:java

Each sample lists its required environment variables in the file header.

Testing

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 test

Define a data model

from 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 — None lets the server generate a UUID; any other value must be a UUID).
  • One "data" field named content becomes the embedded text (originalContent in 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.

Option A: Wired into a Semantic Kernel agent

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())

Option B: Single collection

see example_single_collection.py

Option C: Store (multiple collections, shared connection)

see example_store.py

Behavior notes

  • 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 raises ArgumentException and Java raises IllegalArgumentException. To let the server assign a key, pass None (Python) or null (.NET, Java). In Python the exception depends on what was refused:

    • A key passed to get, upsert or delete raises VectorStoreOperationException. The connector raises ValueError, and Semantic Kernel wraps it.
    • GOODMEM_RERANKER_ID raises VectorSearchExecutionException from search. That is a subclass of VectorStoreOperationException.
    • GOODMEM_EMBEDDER_ID raises VectorStoreInitializationException from ensure_collection_exists. That is not a VectorStoreOperationException, so except VectorStoreOperationException does not catch it. Semantic Kernel wraps it in VectorStoreOperationException when upsert hits it first, and in VectorSearchExecutionException when search does.
    • A space id the server listed that is not a UUID makes ensure_collection_deleted raise VectorStoreOperationException and 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 upsert raise VectorStoreOperationException. Its __cause__ is a GoodMemUpsertError: that record was written, and written_keys lists the records written before it.
  • 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
    
  • content is write-only in GoodMem. The server does not return originalContent in search responses. Retrieved text comes from chunkText (a chunk of the original), which the connector maps back to your content field transparently.

  • Score convention. A GoodMem vector relevanceScore is 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 roughly 0.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 reports RERANKING_FAILED (and, when the id names no reranker, a NOT_FOUND naming it) and still returns its vector hits. The connector scores those as vector hits, so the best match still scores highest. KernelSearchResults.metadata has goodmem_partial set to True and the codes in goodmem_statuses, and no hit is dropped. A threshold chosen for reranker scores does not fit these scores, so check goodmem_statuses for either code before applying one.

  • A search the server reported a problem with says so (.NET, Java). GoodMem sends status events in a search's response stream, for example NOT_FOUND and RERANKING_FAILED when a reranker does not exist, or EMBEDDER_FAILED. The connectors follow the retrieval status contract every GoodMem integration follows:

    • FEATURE_DISABLED and LLM_CAPABILITY_INFERRED are 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 in OriginalCode (.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 a GoodMemSearchResults<TRecord> with Results, Partial and Statuses; searchWithStatus (Java) returns a GoodMemCollection.SearchResults<T> with results(), partial() and statuses(). Each status is a GoodMemRetrievalStatus:

    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 Java search and the Java plugin's recall have nowhere to put the flag, so they log a warning that names the statuses whenever the search was partial. SearchWithStatusAsync and searchWithStatus log one only when a partial search returned nothing. .NET logs through GoodMemOptions.LoggerFactory, or to standard error when it is not set (set NullLoggerFactory.Instance to silence it). Java logs through System.Logger under ai.goodmem.semantickernel.GoodMemCollection, which java.util.logging prints 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 with tag and year data fields:

    await collection.search("quarterly", filter=lambda n: n.tag == "finance" and n.year > 2000)
    

    ==, !=, <, <=, >, >=, in, not in, and, or and not are supported. Values are quoted and cast for you — a value containing an apostrophe is a value, not syntax — and a boolean is compared with a BOOLEAN cast, 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; the content field is the embedded body, not metadata. The .NET connector raises NotSupportedException for a filter, and the Java search takes none.

  • Pre-computed vectors not supported. Passing vector= to search() raises VectorStoreOperationNotSupportedException (Python); a non-string search value raises NotSupportedException (.NET). Pass text only.

Project structure

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

API reference (Python)

GoodMemCollection

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

GoodMemStore

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

GoodMemSettings

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,
)

Metadata

Release files for goodmem-semantic-kernel 0.3.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for goodmem-semantic-kernel 0.3.2
File Size Uploaded
goodmem_semantic_kernel-0.3.2.tar.gz 133.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for goodmem-semantic-kernel 0.3.2
File Interpreter ABI Platform
goodmem_semantic_kernel-0.3.2-py3-none-any.whl Python 3 none any Details

Total release size: 164.9 kB

Release files / goodmem_semantic_kernel-0.3.2.tar.gz

Download URL goodmem_semantic_kernel-0.3.2.tar.gz
Size 133.9 kB
Tags Source
SHA-256 checksum
How to use checksums
1385a8e5f5a338e77dfcccc4496c0622f9412f767fdfc0a5fed171450c96a13e
BLAKE2b-256 checksum
How to use checksums
6cc8fd7c07c19d8a461fd72fc2ea2468fe4e36c19040bb0f015b26a9b809552b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release files / goodmem_semantic_kernel-0.3.2-py3-none-any.whl

Download URL goodmem_semantic_kernel-0.3.2-py3-none-any.whl
Size 31.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1ccfe5c757b12f619f34e222ade2075e69a3a9c280143f084c8e78dbb56be694
BLAKE2b-256 checksum
How to use checksums
e4a4da483b1eccb560c4b8fb4cdbff68c2c29da748db146ff0a9788aa7a2777a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.2 This release

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page