feat(integrations): add Zapier integration for Mem0
Zapier Platform CLI app (zapier-mem0) with Add Memory / Delete Memory
creates and Search Memories / Get Memories searches against the Mem0 v3
API. Custom API-key auth (Authorization: Token), base-URL + auth
injected via beforeRequest middleware, non-2xx surfaced as errors.
Add Memory polls GET /v1/event/{id}/ for async extraction. Includes an
E2E test suite via createAppTester.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
.env
|
||||
.zapierapprc
|
||||
build/
|
||||
coverage/
|
||||
*.log
|
||||
.DS_Store
|
||||
@@ -0,0 +1,33 @@
|
||||
# Zapier integration for Mem0
|
||||
|
||||
A [Zapier](https://zapier.com) integration for [Mem0](https://mem0.ai) — the memory layer for AI agents. Add, search, list, and delete long-term memories from any Zap.
|
||||
|
||||
Built with the [Zapier Platform CLI](https://docs.zapier.com/platform/quickstart/cli-tutorial).
|
||||
|
||||
## Actions
|
||||
|
||||
| Type | Name | Endpoint |
|
||||
| --- | --- | --- |
|
||||
| Create | **Add Memory** | `POST /v3/memories/add/` |
|
||||
| Create | **Delete Memory** | `DELETE /v1/memories/{id}/` |
|
||||
| Search | **Search Memories** | `POST /v3/memories/search/` |
|
||||
| Search | **Get Memories** | `POST /v3/memories/` |
|
||||
|
||||
**Add Memory** runs LLM extraction asynchronously by default; the action polls the event until it completes and returns the resulting memories. Turn off **Wait for Completion** to return immediately, or set **Infer = false** to store verbatim (synchronous).
|
||||
|
||||
## Authentication
|
||||
|
||||
Custom (API key) auth. Provide a Mem0 API key from [app.mem0.ai](https://app.mem0.ai) → Settings → API Keys. It is sent as `Authorization: Token <key>`.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
MEM0_API_KEY=m0-... npm test # runs the E2E suite against api.mem0.ai
|
||||
```
|
||||
|
||||
To deploy (maintainers): `zapier login && zapier push`.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
// Custom (API key) authentication for Mem0.
|
||||
// The key is sent as "Authorization: Token <apiKey>" (matches Mem0's SDKs).
|
||||
const test = (z, _bundle) =>
|
||||
z.request({ url: '/v1/ping/', method: 'GET' });
|
||||
|
||||
module.exports = {
|
||||
type: 'custom',
|
||||
test,
|
||||
fields: [
|
||||
{
|
||||
key: 'apiKey',
|
||||
label: 'Mem0 API Key',
|
||||
type: 'string',
|
||||
required: true,
|
||||
helpText:
|
||||
'Your Mem0 API key (starts with `m0-`). Create one at [app.mem0.ai](https://app.mem0.ai) → Settings → API Keys.',
|
||||
},
|
||||
{
|
||||
key: 'baseUrl',
|
||||
label: 'Base URL',
|
||||
type: 'string',
|
||||
required: false,
|
||||
default: 'https://api.mem0.ai',
|
||||
helpText: 'Override only for self-hosted or non-default deployments.',
|
||||
},
|
||||
],
|
||||
// Shown on the connection label in the Zap editor.
|
||||
connectionLabel: 'Mem0 ({{bundle.authData.baseUrl}})',
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
'use strict';
|
||||
|
||||
const POLL_INTERVAL_MS = 1500;
|
||||
const MAX_POLL_ATTEMPTS = 40; // ~60s
|
||||
|
||||
// Polls GET /v1/event/{id}/ until the async memory-addition event resolves.
|
||||
const pollEvent = async (z, eventId) => {
|
||||
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
|
||||
const res = await z.request({ url: `/v1/event/${eventId}/`, method: 'GET' });
|
||||
const status = res.data && res.data.status;
|
||||
if (status === 'SUCCEEDED' || status === 'FAILED') {
|
||||
return res.data;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
||||
}
|
||||
throw new Error(`Timed out waiting for memory event ${eventId} to complete`);
|
||||
};
|
||||
|
||||
const perform = async (z, bundle) => {
|
||||
const body = {
|
||||
messages: [{ role: bundle.inputData.role || 'user', content: bundle.inputData.content }],
|
||||
infer: bundle.inputData.infer !== undefined ? bundle.inputData.infer : true,
|
||||
};
|
||||
if (bundle.inputData.user_id) body.user_id = bundle.inputData.user_id;
|
||||
if (bundle.inputData.agent_id) body.agent_id = bundle.inputData.agent_id;
|
||||
if (bundle.inputData.run_id) body.run_id = bundle.inputData.run_id;
|
||||
if (bundle.inputData.metadata) {
|
||||
body.metadata =
|
||||
typeof bundle.inputData.metadata === 'string'
|
||||
? JSON.parse(bundle.inputData.metadata)
|
||||
: bundle.inputData.metadata;
|
||||
}
|
||||
|
||||
const response = await z.request({
|
||||
url: '/v3/memories/add/',
|
||||
method: 'POST',
|
||||
body,
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
const wait = bundle.inputData.waitForCompletion !== false;
|
||||
|
||||
// infer=true returns {event_id, status:PENDING}; optionally wait for the result.
|
||||
if (wait && data.status && data.status !== 'SUCCEEDED' && data.event_id) {
|
||||
return pollEvent(z, data.event_id);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
key: 'add_memory',
|
||||
noun: 'Memory',
|
||||
display: {
|
||||
label: 'Add Memory',
|
||||
description: 'Extract and store memories from a message.',
|
||||
},
|
||||
operation: {
|
||||
perform,
|
||||
inputFields: [
|
||||
{
|
||||
key: 'content',
|
||||
label: 'Content',
|
||||
type: 'text',
|
||||
required: true,
|
||||
helpText: 'The message content to extract memories from.',
|
||||
},
|
||||
{
|
||||
key: 'role',
|
||||
label: 'Role',
|
||||
choices: { user: 'User', assistant: 'Assistant', system: 'System' },
|
||||
default: 'user',
|
||||
},
|
||||
{ key: 'user_id', label: 'User ID', type: 'string' },
|
||||
{ key: 'agent_id', label: 'Agent ID', type: 'string' },
|
||||
{ key: 'run_id', label: 'Run ID', type: 'string' },
|
||||
{ key: 'metadata', label: 'Metadata (JSON)', type: 'string' },
|
||||
{
|
||||
key: 'infer',
|
||||
label: 'Infer',
|
||||
type: 'boolean',
|
||||
default: 'true',
|
||||
helpText: 'Run LLM extraction (async). Turn off to store verbatim (synchronous).',
|
||||
},
|
||||
{
|
||||
key: 'waitForCompletion',
|
||||
label: 'Wait for Completion',
|
||||
type: 'boolean',
|
||||
default: 'true',
|
||||
helpText: 'Poll until extraction finishes and return the resulting memories.',
|
||||
},
|
||||
],
|
||||
sample: { status: 'SUCCEEDED', event_id: '00000000-0000-0000-0000-000000000000', results: [] },
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
const perform = async (z, bundle) => {
|
||||
// Trailing slash required (Django APPEND_SLASH).
|
||||
const response = await z.request({
|
||||
url: `/v1/memories/${bundle.inputData.memory_id}/`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
return response.data || { message: 'Deleted', memory_id: bundle.inputData.memory_id };
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
key: 'delete_memory',
|
||||
noun: 'Memory',
|
||||
display: {
|
||||
label: 'Delete Memory',
|
||||
description: 'Delete a single memory by its ID.',
|
||||
},
|
||||
operation: {
|
||||
perform,
|
||||
inputFields: [
|
||||
{ key: 'memory_id', label: 'Memory ID', type: 'string', required: true },
|
||||
],
|
||||
sample: { message: 'Memory deleted successfully' },
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
|
||||
const authentication = require('./authentication');
|
||||
const { includeApiKey, handleBadResponses } = require('./middleware');
|
||||
|
||||
const addMemory = require('./creates/add_memory');
|
||||
const deleteMemory = require('./creates/delete_memory');
|
||||
const searchMemories = require('./searches/search_memories');
|
||||
const getMemories = require('./searches/get_memories');
|
||||
|
||||
const { version } = require('./package.json');
|
||||
const platformVersion = require('zapier-platform-core').version;
|
||||
|
||||
module.exports = {
|
||||
version,
|
||||
platformVersion,
|
||||
|
||||
authentication,
|
||||
|
||||
beforeRequest: [includeApiKey],
|
||||
afterResponse: [handleBadResponses],
|
||||
|
||||
creates: {
|
||||
[addMemory.key]: addMemory,
|
||||
[deleteMemory.key]: deleteMemory,
|
||||
},
|
||||
|
||||
searches: {
|
||||
[searchMemories.key]: searchMemories,
|
||||
[getMemories.key]: getMemories,
|
||||
},
|
||||
|
||||
resources: {},
|
||||
triggers: {},
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
// Prepend the configured base URL and inject the auth header on every request.
|
||||
const includeApiKey = (request, z, bundle) => {
|
||||
if (bundle.authData && bundle.authData.apiKey) {
|
||||
request.headers = request.headers || {};
|
||||
request.headers.Authorization = `Token ${bundle.authData.apiKey}`;
|
||||
}
|
||||
// Resolve relative URLs against the configured base URL.
|
||||
if (request.url && request.url.startsWith('/')) {
|
||||
const base = (bundle.authData && bundle.authData.baseUrl) || 'https://api.mem0.ai';
|
||||
request.url = `${base.replace(/\/$/, '')}${request.url}`;
|
||||
}
|
||||
return request;
|
||||
};
|
||||
|
||||
// Surface auth failures as Zapier auth errors so users are prompted to reconnect.
|
||||
const handleBadResponses = (response, z, _bundle) => {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new z.errors.Error(
|
||||
'The Mem0 API key you supplied is invalid or lacks access.',
|
||||
'AuthenticationError',
|
||||
response.status,
|
||||
);
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
module.exports = { includeApiKey, handleBadResponses };
|
||||
+4512
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "zapier-mem0",
|
||||
"version": "0.1.0",
|
||||
"description": "Zapier integration for Mem0 — the memory layer for AI agents.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mem0ai/mem0",
|
||||
"directory": "integrations/zapier-mem0"
|
||||
},
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "jest --testTimeout 120000"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
"npm": ">=5.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"zapier-platform-core": "19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.7.0"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
const perform = async (z, bundle) => {
|
||||
const body = {};
|
||||
if (bundle.inputData.user_id) body.filters = { user_id: bundle.inputData.user_id };
|
||||
|
||||
const response = await z.request({
|
||||
url: '/v3/memories/',
|
||||
method: 'POST',
|
||||
params: { page: 1, page_size: bundle.inputData.limit || 50 },
|
||||
body,
|
||||
});
|
||||
const data = response.data;
|
||||
return Array.isArray(data) ? data : data.results || [];
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
key: 'get_memories',
|
||||
noun: 'Memory',
|
||||
display: {
|
||||
label: 'Get Memories',
|
||||
description: 'List stored memories for a user.',
|
||||
},
|
||||
operation: {
|
||||
perform,
|
||||
inputFields: [
|
||||
{ key: 'user_id', label: 'User ID', type: 'string' },
|
||||
{ key: 'limit', label: 'Limit', type: 'integer', default: '50' },
|
||||
],
|
||||
sample: { id: '00000000-0000-0000-0000-000000000000', memory: 'User loves hiking' },
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
|
||||
const perform = async (z, bundle) => {
|
||||
const body = {
|
||||
query: bundle.inputData.query,
|
||||
output_format: 'v1.1',
|
||||
limit: bundle.inputData.limit || 10,
|
||||
};
|
||||
if (bundle.inputData.user_id) body.filters = { user_id: bundle.inputData.user_id };
|
||||
|
||||
const response = await z.request({
|
||||
url: '/v3/memories/search/',
|
||||
method: 'POST',
|
||||
body,
|
||||
});
|
||||
// Searches must return an array.
|
||||
const data = response.data;
|
||||
return Array.isArray(data) ? data : data.results || [];
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
key: 'search_memories',
|
||||
noun: 'Memory',
|
||||
display: {
|
||||
label: 'Search Memories',
|
||||
description: 'Semantic search over stored memories.',
|
||||
},
|
||||
operation: {
|
||||
perform,
|
||||
inputFields: [
|
||||
{ key: 'query', label: 'Query', type: 'string', required: true },
|
||||
{ key: 'user_id', label: 'User ID', type: 'string' },
|
||||
{ key: 'limit', label: 'Limit', type: 'integer', default: '10' },
|
||||
],
|
||||
sample: { id: '00000000-0000-0000-0000-000000000000', memory: 'User loves hiking' },
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
'use strict';
|
||||
|
||||
/* global describe, it, expect, beforeAll */
|
||||
|
||||
const zapier = require('zapier-platform-core');
|
||||
const App = require('../index');
|
||||
|
||||
const appTester = zapier.createAppTester(App);
|
||||
|
||||
const authData = {
|
||||
apiKey: process.env.MEM0_API_KEY,
|
||||
baseUrl: process.env.MEM0_BASE_URL || 'https://api.mem0.ai',
|
||||
};
|
||||
|
||||
const userId = `zapier-e2e-${Date.now()}`;
|
||||
|
||||
describe('Mem0 Zapier integration (E2E)', () => {
|
||||
beforeAll(() => {
|
||||
if (!authData.apiKey) throw new Error('MEM0_API_KEY env var is required for E2E tests');
|
||||
});
|
||||
|
||||
it('authentication.test succeeds', async () => {
|
||||
const res = await appTester(App.authentication.test, { authData });
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it('adds, searches, lists, and deletes a memory', async () => {
|
||||
// Add (async, wait for completion)
|
||||
const added = await appTester(App.creates.add_memory.operation.perform, {
|
||||
authData,
|
||||
inputData: {
|
||||
content: 'I love hiking in the Alps and my favorite food is sushi',
|
||||
user_id: userId,
|
||||
waitForCompletion: true,
|
||||
},
|
||||
});
|
||||
expect(added.status).toBe('SUCCEEDED');
|
||||
|
||||
// Search
|
||||
const found = await appTester(App.searches.search_memories.operation.perform, {
|
||||
authData,
|
||||
inputData: { query: 'outdoor activities', user_id: userId, limit: 5 },
|
||||
});
|
||||
expect(Array.isArray(found)).toBe(true);
|
||||
expect(found.length).toBeGreaterThan(0);
|
||||
|
||||
// Get all
|
||||
const all = await appTester(App.searches.get_memories.operation.perform, {
|
||||
authData,
|
||||
inputData: { user_id: userId },
|
||||
});
|
||||
expect(Array.isArray(all)).toBe(true);
|
||||
expect(all.length).toBeGreaterThan(0);
|
||||
|
||||
// Cleanup: delete every memory we created
|
||||
for (const mem of all) {
|
||||
await appTester(App.creates.delete_memory.operation.perform, {
|
||||
authData,
|
||||
inputData: { memory_id: mem.id },
|
||||
});
|
||||
}
|
||||
|
||||
const afterDelete = await appTester(App.searches.get_memories.operation.perform, {
|
||||
authData,
|
||||
inputData: { user_id: userId },
|
||||
});
|
||||
expect(afterDelete.length).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user