Skip to content

feat(js/plugins/goodmem): add GoodMem plugin#5373

Closed
MohamedKhalil8801 wants to merge 10 commits into
genkit-ai:mainfrom
PAIR-Systems-Inc:main
Closed

feat(js/plugins/goodmem): add GoodMem plugin#5373
MohamedKhalil8801 wants to merge 10 commits into
genkit-ai:mainfrom
PAIR-Systems-Inc:main

Conversation

@MohamedKhalil8801

Copy link
Copy Markdown

Closes #5372.

Adds a first-party genkitx-goodmem plugin that exposes GoodMem, a retrieval-augmented generation (RAG) memory service, as Genkit tools.

Contents

  • js/plugins/goodmem/: plugin source plus 34 unit tests with mocked fetch.
  • js/testapps/goodmem/: runnable demo with three scenarios (persistent project context across sessions, a scribe-and-analyst pipeline using Genkit span enrichment, and a structured team activity log with metadata-driven retrieval).

11 tools are registered under the goodmem/ namespace: list_embedders, list_spaces, get_space, create_space, update_space, delete_space, create_memory, list_memories, retrieve_memories, get_memory, delete_memory.

Usage

import { genkit } from 'genkit';
import { goodmem } from 'genkitx-goodmem';

const ai = genkit({
  plugins: [
    goodmem({
      baseUrl: process.env.GOODMEM_BASE_URL || 'http://localhost:8080',
      apiKey: process.env.GOODMEM_API_KEY!,
    }),
  ],
});

Once loaded, the tools work with any Genkit agent or flow. For example, retrieving memories scoped by metadata:

const retrieve = await ai.registry.lookupAction(
  '/tool/goodmem/retrieve_memories'
);
const { results } = await retrieve({
  query: 'Show me the new features we shipped this cycle.',
  spaceIds: [spaceId],
  metadataFilter: "CAST(val('$.category') AS TEXT) = 'feat'",
});

See js/plugins/goodmem/README.md for the full tool reference and js/testapps/goodmem/src/index.ts for the three-scenario demo.

Verification

  • pnpm build in js/plugins/goodmem and js/testapps/goodmem: clean.
  • pnpm test in js/plugins/goodmem: 34 / 34 pass.
  • Live run of the testapp against a running GoodMem server and OpenAI gpt-4o-mini produces correct grounded answers across all three scenarios.

Checklist (if applicable):

bashareid and others added 9 commits March 31, 2026 18:51
Drop two provenance comments and four em dashes from the plugin source.
Reframe the goodmem() JSDoc and the package description to mark GoodMem as a
retrieval-augmented generation memory backend. The test file picks up one
inline em dash fix and an alphabetical import reorder. No behavior changes;
tests pass 25 of 25.
…emories

Expose seven server-side GoodMem features as optional parameters across two
tools.

retrieve_memories gains:
- metadataFilter: SQL-style JSONPath expression applied server-side to every
  space in spaceIds. Enables category, tag, or any-metadata filtering at
  query time.
- maxWaitSeconds and pollInterval: per-call polling configuration. The
  previous 10s wait and 2s poll were hardcoded.

list_memories gains:
- statusFilter, includeContent, sortBy, sortOrder. These map to the matching
  server-side query parameters on GET /v1/spaces/{spaceId}/memories.

Nine new unit tests cover the additions: three for metadataFilter (applied
to every space, omitted when unset, empty string treated as unset), two for
the polling timeouts (custom values honored, polling skipped when
waitForIndexing is false), and four for the list_memories query string (all
filters set, none set, includeContent=false omitted, partial set). Test
suite: 34 of 34 pass.
- Reframe the intro: GoodMem is a retrieval-augmented generation memory
  backend for AI agents.
- Per-tool input bullets use colons instead of em dashes.
- Document the seven new optional parameters on retrieve_memories
  (metadataFilter, maxWaitSeconds, pollInterval) and list_memories
  (statusFilter, includeContent, sortBy, sortOrder).
- Rephrase the waitForIndexing description so it points at maxWaitSeconds
  rather than the hardcoded 10 seconds.
- Add a pointer to the runnable demo at js/testapps/goodmem/.
Add a runnable demo at js/testapps/goodmem that exercises the plugin against
a live GoodMem server and an OpenAI-compatible model. Three scenarios:

1. Persistent project knowledge. Stores five facts about a fictional company,
   retrieves the top chunks for a semantic query, and asks the model to
   answer the question grounded in the retrieved context.
2. Scribe and analyst pipeline. The scribe stores team notes inside one
   Genkit span (role=scribe); the analyst retrieves and summarizes them
   inside another (role=analyst). Roles show up as labels in the trace.
3. Metadata-driven retrieval. Stores release entries tagged by category and
   uses the metadataFilter parameter to pull back only the 'feat' entries.

Each scenario creates its own space; the cleanup phase deletes all three at
the end. Two run modes:

  pnpm demo          plain tsx, prints to stdout
  pnpm genkit:start  wraps with the Genkit Developer UI at
                     http://localhost:4000 for trace inspection

The lockfile picks up the new workspace member. It also reconciles the
existing goodmem plugin entry: the lockfile previously listed genkit under
dependencies, but the plugin's package.json declares it only under
devDependencies and peerDependencies. pnpm install brought the two back into
sync.
- Scenario 1 stores three project notes about a customer support assistant
  on Python 3.12, FastAPI, Postgres, and pytest with 80% coverage. The
  question is "Remind me what our coverage requirement is."
- Scenario 2's team notes use "under 2 hours" instead of "under two hours"
  and "Team retro:" instead of "Retro outcome:". The analyst asks "What do
  we know about our services and current priorities?"
- Scenario 3 asks "Show me the new features we've shipped." Release
  entries are unchanged.
- Section titles and the module docstring read as: persistent project
  context across sessions, two-role team knowledge pipeline, structured
  team activity log.

Live run still produces correct grounded answers across all three scenarios.
…ntegration

Feature/improve goodmem integration
# Conflicts:
#	js/pnpm-lock.yaml
@github-actions github-actions Bot added docs Improvements or additions to documentation js config labels May 21, 2026
@google-cla

google-cla Bot commented May 21, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the genkitx-goodmem plugin, which integrates the GoodMem RAG memory service into Genkit by providing 11 specialized tools for managing spaces and memories. The implementation includes a comprehensive test suite and a demo application showcasing various RAG scenarios. Review feedback recommends replacing synchronous file system operations with asynchronous ones to avoid blocking the event loop and correcting error handling logic that incorrectly attempts to access properties on standard Error objects. Additionally, a discrepancy in the default base URL protocol within the demo application was identified for correction to ensure local connectivity.

Comment thread js/plugins/goodmem/src/index.ts Outdated
Comment on lines +810 to +830
if (!fs.existsSync(filePath)) {
return {
success: false,
error: `File not found: ${filePath}`,
};
}
const fileBuffer = fs.readFileSync(filePath);
const base64 = fileBuffer.toString('base64');
const ext = path.extname(filePath).replace('.', '');
const detectedMime = getMimeType(ext);
const mimeType = detectedMime || 'application/octet-stream';
fileName = path.basename(filePath);

if (mimeType.startsWith('text/')) {
requestBody.contentType = mimeType;
requestBody.originalContent = fileBuffer.toString('utf-8');
} else {
requestBody.contentType = mimeType;
requestBody.originalContentB64 = base64;
}
} else if (textContent) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using synchronous file system methods like fs.existsSync and fs.readFileSync blocks the Node.js event loop, which can impact performance. It is recommended to use the asynchronous fs.promises API. Additionally, when a tool fails, instead of returning an error object, add a message to the history queue to inform the LLM of the error and return control, allowing the LLM to attempt a correction. Minor optimizations regarding the placement of base64 conversion should be avoided if the current logic is functionally correct.

        if (filePath) {
          try {
            const fileBuffer = await fs.promises.readFile(filePath);
            const ext = path.extname(filePath).replace('.', '');
            const detectedMime = getMimeType(ext);
            const mimeType = detectedMime || 'application/octet-stream';
            fileName = path.basename(filePath);

            requestBody.contentType = mimeType;
            if (mimeType.startsWith('text/')) {
              requestBody.originalContent = fileBuffer.toString('utf-8');
            } else {
              requestBody.originalContentB64 = fileBuffer.toString('base64');
            }
          } catch (e: any) {
            history.push({ role: 'user', content: `Failed to read file at ${filePath}: ${e.message}` });
            return;
          }
        }
References
  1. When a tool fails, instead of throwing an error or returning a ToolResponse, add a message to the history queue to inform the LLM of the error and return control.
  2. Avoid making changes for minor optimizations if the existing code is functionally correct.

success: false,
error: error.message || 'Failed to list embedders',
details: error.response?.body || String(error),
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The code attempts to access error.response?.body, but the apiJson and apiFetch helpers throw standard Error objects. Furthermore, when a tool fails, instead of returning a failure response or letting the error propagate, add a message to the history queue to inform the LLM of the error and return control, allowing it to attempt a correction.

References
  1. When a tool fails, instead of throwing an error or returning a ToolResponse, add a message to the history queue to inform the LLM of the error and return control.

import { runInNewSpan } from 'genkit/tracing';
import { goodmem } from 'genkitx-goodmem';

const BASE_URL = process.env.GOODMEM_BASE_URL ?? 'https://localhost:8080';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The default BASE_URL uses https://localhost:8080, while the plugin's README and typical local GoodMem setups use http://localhost:8080. This discrepancy may cause connection failures for users running the demo locally without a custom environment variable.

Suggested change
const BASE_URL = process.env.GOODMEM_BASE_URL ?? 'https://localhost:8080';
const BASE_URL = process.env.GOODMEM_BASE_URL ?? 'http://localhost:8080';

@MichaelDoyle

MichaelDoyle commented May 21, 2026

Copy link
Copy Markdown
Contributor

Hey @MohamedKhalil8801 - excited to see this come to Genkit, thanks for the contribution!

We're not accepting 3P plugin submissions to the repo at this time. Our recommendation is to publish it to NPM with the prefix genkitx-* (see: https://www.npmjs.com/search?q=genkitx).

- Switch create_memory from fs.existsSync + fs.readFileSync to
  fs.promises.readFile so the tool does not block the Node.js event
  loop while reading file content from disk. ENOENT is still surfaced
  as a "File not found" success:false response; other read errors are
  reported with their message.
- Drop the dead `error.response?.body` access from every tool's catch
  block. apiJson and apiFetch throw plain Error objects, so .response
  is always undefined. String(error) carries the formatted "GoodMem
  API error (status): detail" message on its own.
- Update the plugin README and the goodmem() JSDoc examples to use
  https://localhost:8080, matching the demo testapp default and the
  typical GoodMem deployment.

All 34 unit tests still pass.
@MichaelDoyle

Copy link
Copy Markdown
Contributor

Closing per my earlier explanation. Thanks again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

config docs Improvements or additions to documentation js

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add GoodMem plugin

3 participants