@pentatonic-ai/ai-agent-sdk 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/cli.js +1 -1
- package/dist/index.cjs +25 -9
- package/dist/index.js +25 -9
- package/package.json +1 -1
- package/packages/memory/openclaw-plugin/README.md +60 -0
- package/packages/memory/openclaw-plugin/index.js +580 -0
- package/packages/memory/openclaw-plugin/openclaw.plugin.json +69 -0
- package/packages/memory/openclaw-plugin/package.json +31 -0
- package/packages/memory/src/openclaw/package.json +1 -1
- package/packages/memory/src/search.js +17 -0
- package/src/transport.js +29 -13
package/README.md
CHANGED
|
@@ -171,7 +171,7 @@ Works with both local and hosted setups. Just tell OpenClaw to set it up.
|
|
|
171
171
|
### Install
|
|
172
172
|
|
|
173
173
|
```bash
|
|
174
|
-
openclaw plugins install @pentatonic-ai/
|
|
174
|
+
openclaw plugins install @pentatonic-ai/openclaw-memory-plugin
|
|
175
175
|
```
|
|
176
176
|
|
|
177
177
|
### Set up
|
package/bin/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { createInterface } from "readline";
|
|
4
4
|
import { execFileSync } from "child_process";
|
|
5
|
-
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
import { homedir } from "os";
|
|
8
8
|
|
package/dist/index.cjs
CHANGED
|
@@ -120,18 +120,36 @@ function parseArgs(args) {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
// src/transport.js
|
|
123
|
-
var
|
|
124
|
-
mutation
|
|
125
|
-
|
|
123
|
+
var CREATE_MODULE_EVENT_MUTATION = `
|
|
124
|
+
mutation CreateModuleEvent($moduleId: String!, $input: ModuleEventInput!) {
|
|
125
|
+
createModuleEvent(moduleId: $moduleId, input: $input) {
|
|
126
126
|
success
|
|
127
127
|
eventId
|
|
128
|
-
message
|
|
129
128
|
}
|
|
130
129
|
}
|
|
131
130
|
`;
|
|
131
|
+
function getModuleId(eventType) {
|
|
132
|
+
if (["STORE_MEMORY", "SESSION_START", "SESSION_END"].includes(eventType)) {
|
|
133
|
+
return "deep-memory";
|
|
134
|
+
}
|
|
135
|
+
return "conversation-analytics";
|
|
136
|
+
}
|
|
132
137
|
async function sendEvent({ endpoint, apiKey, clientId, userId, headers }, input, fetchFn) {
|
|
133
138
|
const f = fetchFn || globalThis.fetch;
|
|
134
139
|
const authHeaders = apiKey.startsWith("tes_") ? { Authorization: `Bearer ${apiKey}` } : { "x-service-key": apiKey };
|
|
140
|
+
const moduleId = getModuleId(input.eventType);
|
|
141
|
+
const attributes = {
|
|
142
|
+
...input.data?.attributes,
|
|
143
|
+
clientId,
|
|
144
|
+
...userId ? { userId } : {}
|
|
145
|
+
};
|
|
146
|
+
const moduleInput = {
|
|
147
|
+
eventType: input.eventType,
|
|
148
|
+
data: {
|
|
149
|
+
entity_id: input.data?.entity_id || input.entityId || "",
|
|
150
|
+
attributes
|
|
151
|
+
}
|
|
152
|
+
};
|
|
135
153
|
const response = await f(`${endpoint}/api/graphql`, {
|
|
136
154
|
method: "POST",
|
|
137
155
|
headers: {
|
|
@@ -141,10 +159,8 @@ async function sendEvent({ endpoint, apiKey, clientId, userId, headers }, input,
|
|
|
141
159
|
...authHeaders
|
|
142
160
|
},
|
|
143
161
|
body: JSON.stringify({
|
|
144
|
-
query:
|
|
145
|
-
variables: {
|
|
146
|
-
input: userId ? { ...input, data: { ...input.data, attributes: { ...input.data?.attributes, userId } } } : input
|
|
147
|
-
}
|
|
162
|
+
query: CREATE_MODULE_EVENT_MUTATION,
|
|
163
|
+
variables: { moduleId, input: moduleInput }
|
|
148
164
|
})
|
|
149
165
|
});
|
|
150
166
|
if (!response.ok) {
|
|
@@ -154,7 +170,7 @@ async function sendEvent({ endpoint, apiKey, clientId, userId, headers }, input,
|
|
|
154
170
|
if (json.errors?.length) {
|
|
155
171
|
throw new Error(`TES GraphQL error: ${json.errors[0].message}`);
|
|
156
172
|
}
|
|
157
|
-
return json.data.
|
|
173
|
+
return json.data.createModuleEvent;
|
|
158
174
|
}
|
|
159
175
|
|
|
160
176
|
// src/tracking.js
|
package/dist/index.js
CHANGED
|
@@ -89,18 +89,36 @@ function parseArgs(args) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// src/transport.js
|
|
92
|
-
var
|
|
93
|
-
mutation
|
|
94
|
-
|
|
92
|
+
var CREATE_MODULE_EVENT_MUTATION = `
|
|
93
|
+
mutation CreateModuleEvent($moduleId: String!, $input: ModuleEventInput!) {
|
|
94
|
+
createModuleEvent(moduleId: $moduleId, input: $input) {
|
|
95
95
|
success
|
|
96
96
|
eventId
|
|
97
|
-
message
|
|
98
97
|
}
|
|
99
98
|
}
|
|
100
99
|
`;
|
|
100
|
+
function getModuleId(eventType) {
|
|
101
|
+
if (["STORE_MEMORY", "SESSION_START", "SESSION_END"].includes(eventType)) {
|
|
102
|
+
return "deep-memory";
|
|
103
|
+
}
|
|
104
|
+
return "conversation-analytics";
|
|
105
|
+
}
|
|
101
106
|
async function sendEvent({ endpoint, apiKey, clientId, userId, headers }, input, fetchFn) {
|
|
102
107
|
const f = fetchFn || globalThis.fetch;
|
|
103
108
|
const authHeaders = apiKey.startsWith("tes_") ? { Authorization: `Bearer ${apiKey}` } : { "x-service-key": apiKey };
|
|
109
|
+
const moduleId = getModuleId(input.eventType);
|
|
110
|
+
const attributes = {
|
|
111
|
+
...input.data?.attributes,
|
|
112
|
+
clientId,
|
|
113
|
+
...userId ? { userId } : {}
|
|
114
|
+
};
|
|
115
|
+
const moduleInput = {
|
|
116
|
+
eventType: input.eventType,
|
|
117
|
+
data: {
|
|
118
|
+
entity_id: input.data?.entity_id || input.entityId || "",
|
|
119
|
+
attributes
|
|
120
|
+
}
|
|
121
|
+
};
|
|
104
122
|
const response = await f(`${endpoint}/api/graphql`, {
|
|
105
123
|
method: "POST",
|
|
106
124
|
headers: {
|
|
@@ -110,10 +128,8 @@ async function sendEvent({ endpoint, apiKey, clientId, userId, headers }, input,
|
|
|
110
128
|
...authHeaders
|
|
111
129
|
},
|
|
112
130
|
body: JSON.stringify({
|
|
113
|
-
query:
|
|
114
|
-
variables: {
|
|
115
|
-
input: userId ? { ...input, data: { ...input.data, attributes: { ...input.data?.attributes, userId } } } : input
|
|
116
|
-
}
|
|
131
|
+
query: CREATE_MODULE_EVENT_MUTATION,
|
|
132
|
+
variables: { moduleId, input: moduleInput }
|
|
117
133
|
})
|
|
118
134
|
});
|
|
119
135
|
if (!response.ok) {
|
|
@@ -123,7 +139,7 @@ async function sendEvent({ endpoint, apiKey, clientId, userId, headers }, input,
|
|
|
123
139
|
if (json.errors?.length) {
|
|
124
140
|
throw new Error(`TES GraphQL error: ${json.errors[0].message}`);
|
|
125
141
|
}
|
|
126
|
-
return json.data.
|
|
142
|
+
return json.data.createModuleEvent;
|
|
127
143
|
}
|
|
128
144
|
|
|
129
145
|
// src/tracking.js
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pentatonic-ai/ai-agent-sdk",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "TES SDK — LLM observability and lifecycle tracking via Pentatonic Thing Event System. Track token usage, tool calls, and conversations. Manage things through event-sourced lifecycle stages with AI enrichment and vector search.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# @pentatonic-ai/openclaw-memory-plugin
|
|
2
|
+
|
|
3
|
+
Persistent, searchable memory for OpenClaw. Local (Docker + Ollama) or hosted (Pentatonic TES).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
openclaw plugins install @pentatonic-ai/openclaw-memory-plugin
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
Tell OpenClaw:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Set up pentatonic memory
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or configure manually in `openclaw.json`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"plugins": {
|
|
24
|
+
"slots": { "contextEngine": "pentatonic-memory" },
|
|
25
|
+
"entries": {
|
|
26
|
+
"pentatonic-memory": {
|
|
27
|
+
"enabled": true,
|
|
28
|
+
"config": {
|
|
29
|
+
"database_url": "postgres://memory:memory@localhost:5433/memory",
|
|
30
|
+
"embedding_url": "http://localhost:11435/v1",
|
|
31
|
+
"embedding_model": "nomic-embed-text",
|
|
32
|
+
"llm_url": "http://localhost:11435/v1",
|
|
33
|
+
"llm_model": "llama3.2:3b"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## What it does
|
|
42
|
+
|
|
43
|
+
Every lifecycle event is handled automatically:
|
|
44
|
+
|
|
45
|
+
- **Ingest** — every message stored with embeddings + HyDE query expansion
|
|
46
|
+
- **Assemble** — relevant memories injected as context before every prompt
|
|
47
|
+
- **Compact** — decay cycle when context window fills
|
|
48
|
+
- **After turn** — high-access memories consolidated to semantic layer
|
|
49
|
+
|
|
50
|
+
Plus tools: `memory_search`, `memory_store`, `memory_layers`
|
|
51
|
+
|
|
52
|
+
## Local vs Hosted
|
|
53
|
+
|
|
54
|
+
**Local**: Fully private. Requires Docker (Postgres + pgvector + Ollama). Run `npx @pentatonic-ai/ai-agent-sdk memory` to set up.
|
|
55
|
+
|
|
56
|
+
**Hosted**: Connect to Pentatonic TES for higher-dimensional embeddings, team memory, and analytics. Run `npx @pentatonic-ai/ai-agent-sdk init`.
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pentatonic Memory — OpenClaw Context Engine Plugin
|
|
3
|
+
*
|
|
4
|
+
* Install: openclaw plugins install @pentatonic-ai/openclaw-memory-plugin
|
|
5
|
+
*
|
|
6
|
+
* Provides persistent, searchable memory via the ContextEngine lifecycle:
|
|
7
|
+
* ingest — every message stored with embedding + HyDE
|
|
8
|
+
* assemble — relevant memories injected before every prompt
|
|
9
|
+
* compact — decay cycle on context overflow
|
|
10
|
+
* afterTurn — consolidation check
|
|
11
|
+
*
|
|
12
|
+
* Plus agent-callable tools: pentatonic_memory_search, pentatonic_memory_store, pentatonic_memory_status, pentatonic_memory_setup
|
|
13
|
+
*
|
|
14
|
+
* Two modes:
|
|
15
|
+
* - Local: HTTP calls to the memory server (localhost:3333)
|
|
16
|
+
* - Hosted: HTTP calls to TES GraphQL API
|
|
17
|
+
*
|
|
18
|
+
* No native modules, no child_process, no filesystem access.
|
|
19
|
+
* All config comes from OpenClaw's plugin config system.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const TES_ENDPOINT = "https://api.pentatonic.com";
|
|
23
|
+
|
|
24
|
+
const SUCCESS_GIFS = [
|
|
25
|
+
"https://media.giphy.com/media/l0MYt5jPR6QX5APm0/giphy.gif", // brain expanding
|
|
26
|
+
"https://media.giphy.com/media/3o7btNa0RUYa5E7iiQ/giphy.gif", // elephant never forgets
|
|
27
|
+
"https://media.giphy.com/media/d31vTpVi1LAcDvdm/giphy.gif", // thinking smart
|
|
28
|
+
"https://media.giphy.com/media/3o7buirYcmV5nSwIRW/giphy.gif", // mind blown
|
|
29
|
+
"https://media.giphy.com/media/xT0xeJpnrWC3nQ8S1G/giphy.gif", // remembering
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function randomGif() {
|
|
33
|
+
return SUCCESS_GIFS[Math.floor(Math.random() * SUCCESS_GIFS.length)];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- Stats tracking ---
|
|
37
|
+
|
|
38
|
+
const stats = {
|
|
39
|
+
memoriesInjected: 0,
|
|
40
|
+
memoriesStored: 0,
|
|
41
|
+
searchesRun: 0,
|
|
42
|
+
lastAssembleCount: 0,
|
|
43
|
+
backendReachable: null,
|
|
44
|
+
mode: "unknown",
|
|
45
|
+
setupPrompted: false,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// --- Local mode: HTTP to memory server ---
|
|
49
|
+
|
|
50
|
+
async function localSearch(baseUrl, query, limit = 5, minScore = 0.3) {
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(`${baseUrl}/search`, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
|
+
body: JSON.stringify({ query, limit, min_score: minScore }),
|
|
56
|
+
signal: AbortSignal.timeout(5000),
|
|
57
|
+
});
|
|
58
|
+
if (!res.ok) { stats.backendReachable = false; return []; }
|
|
59
|
+
stats.backendReachable = true;
|
|
60
|
+
const data = await res.json();
|
|
61
|
+
return data.results || [];
|
|
62
|
+
} catch {
|
|
63
|
+
stats.backendReachable = false;
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function localStore(baseUrl, content, metadata = {}) {
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch(`${baseUrl}/store`, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
body: JSON.stringify({ content, metadata }),
|
|
74
|
+
signal: AbortSignal.timeout(10000),
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok) { stats.backendReachable = false; return null; }
|
|
77
|
+
stats.backendReachable = true;
|
|
78
|
+
return res.json();
|
|
79
|
+
} catch {
|
|
80
|
+
stats.backendReachable = false;
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function localHealth(baseUrl) {
|
|
86
|
+
try {
|
|
87
|
+
const res = await fetch(`${baseUrl}/health`, { signal: AbortSignal.timeout(3000) });
|
|
88
|
+
stats.backendReachable = res.ok;
|
|
89
|
+
return res.ok;
|
|
90
|
+
} catch {
|
|
91
|
+
stats.backendReachable = false;
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- Hosted mode: TES GraphQL ---
|
|
97
|
+
|
|
98
|
+
function tesHeaders(config) {
|
|
99
|
+
const headers = {
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
"x-client-id": config.tes_client_id,
|
|
102
|
+
};
|
|
103
|
+
if (config.tes_api_key?.startsWith("tes_")) {
|
|
104
|
+
headers["Authorization"] = `Bearer ${config.tes_api_key}`;
|
|
105
|
+
} else {
|
|
106
|
+
headers["x-service-key"] = config.tes_api_key;
|
|
107
|
+
}
|
|
108
|
+
return headers;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function hostedSearch(config, query, limit = 5, minScore = 0.3) {
|
|
112
|
+
try {
|
|
113
|
+
const res = await fetch(`${config.tes_endpoint}/api/graphql`, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: tesHeaders(config),
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
query: `query($clientId: String!, $query: String!, $limit: Int, $minScore: Float) {
|
|
118
|
+
semanticSearchMemories(clientId: $clientId, query: $query, limit: $limit, minScore: $minScore) {
|
|
119
|
+
id content similarity
|
|
120
|
+
}
|
|
121
|
+
}`,
|
|
122
|
+
variables: { clientId: config.tes_client_id, query, limit, minScore },
|
|
123
|
+
}),
|
|
124
|
+
signal: AbortSignal.timeout(5000),
|
|
125
|
+
});
|
|
126
|
+
if (!res.ok) { stats.backendReachable = false; return []; }
|
|
127
|
+
stats.backendReachable = true;
|
|
128
|
+
const json = await res.json();
|
|
129
|
+
return json.data?.semanticSearchMemories || [];
|
|
130
|
+
} catch {
|
|
131
|
+
stats.backendReachable = false;
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function hostedStore(config, content, metadata = {}) {
|
|
137
|
+
try {
|
|
138
|
+
const res = await fetch(`${config.tes_endpoint}/api/graphql`, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: tesHeaders(config),
|
|
141
|
+
body: JSON.stringify({
|
|
142
|
+
query: `mutation CreateModuleEvent($moduleId: String!, $input: ModuleEventInput!) {
|
|
143
|
+
createModuleEvent(moduleId: $moduleId, input: $input) { success eventId }
|
|
144
|
+
}`,
|
|
145
|
+
variables: {
|
|
146
|
+
moduleId: "deep-memory",
|
|
147
|
+
input: {
|
|
148
|
+
eventType: "STORE_MEMORY",
|
|
149
|
+
data: {
|
|
150
|
+
entity_id: metadata.session_id || "openclaw",
|
|
151
|
+
attributes: { ...metadata, content, source: "openclaw-plugin" },
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
}),
|
|
156
|
+
signal: AbortSignal.timeout(10000),
|
|
157
|
+
});
|
|
158
|
+
if (!res.ok) { stats.backendReachable = false; return null; }
|
|
159
|
+
stats.backendReachable = true;
|
|
160
|
+
return res.json();
|
|
161
|
+
} catch {
|
|
162
|
+
stats.backendReachable = false;
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --- TES account setup via HTTP ---
|
|
168
|
+
|
|
169
|
+
async function tesLogin(email, password, clientId) {
|
|
170
|
+
try {
|
|
171
|
+
const res = await fetch(`${TES_ENDPOINT}/api/enrollment/login`, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: { "Content-Type": "application/json" },
|
|
174
|
+
body: JSON.stringify({ email, password, clientId }),
|
|
175
|
+
signal: AbortSignal.timeout(10000),
|
|
176
|
+
});
|
|
177
|
+
if (!res.ok) return null;
|
|
178
|
+
const data = await res.json();
|
|
179
|
+
return data.tokens?.accessToken || null;
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function tesEnroll(email, password, clientId, region) {
|
|
186
|
+
try {
|
|
187
|
+
const res = await fetch(`${TES_ENDPOINT}/api/enrollment/submit`, {
|
|
188
|
+
method: "POST",
|
|
189
|
+
headers: { "Content-Type": "application/json" },
|
|
190
|
+
body: JSON.stringify({
|
|
191
|
+
clientId,
|
|
192
|
+
companyName: clientId,
|
|
193
|
+
industryType: "technology",
|
|
194
|
+
authProvider: "native",
|
|
195
|
+
adminEmail: email,
|
|
196
|
+
adminPassword: password,
|
|
197
|
+
region: (region || "eu").toLowerCase(),
|
|
198
|
+
}),
|
|
199
|
+
signal: AbortSignal.timeout(15000),
|
|
200
|
+
});
|
|
201
|
+
const data = await res.json();
|
|
202
|
+
if (!res.ok) {
|
|
203
|
+
const errors = data.errors || {};
|
|
204
|
+
if (errors.clientId?.includes("already registered")) {
|
|
205
|
+
return { error: "This client ID is already registered. Ask your admin to invite you, then try again." };
|
|
206
|
+
}
|
|
207
|
+
return { error: data.message || Object.values(errors).join(", ") || "Enrollment failed." };
|
|
208
|
+
}
|
|
209
|
+
return { ok: true };
|
|
210
|
+
} catch (err) {
|
|
211
|
+
return { error: `Connection failed: ${err.message}` };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function tesGetApiKey(accessToken, clientId) {
|
|
216
|
+
// Try enrollment service token first
|
|
217
|
+
try {
|
|
218
|
+
const res = await fetch(`${TES_ENDPOINT}/api/enrollment/service-token?client_id=${clientId}`,
|
|
219
|
+
{ signal: AbortSignal.timeout(5000) });
|
|
220
|
+
if (res.ok) {
|
|
221
|
+
const data = await res.json();
|
|
222
|
+
if (data.token) return data.token;
|
|
223
|
+
}
|
|
224
|
+
} catch { /* fallback */ }
|
|
225
|
+
|
|
226
|
+
// Create via GraphQL
|
|
227
|
+
try {
|
|
228
|
+
const res = await fetch(`${TES_ENDPOINT}/api/graphql`, {
|
|
229
|
+
method: "POST",
|
|
230
|
+
headers: {
|
|
231
|
+
"Content-Type": "application/json",
|
|
232
|
+
Authorization: `Bearer ${accessToken}`,
|
|
233
|
+
},
|
|
234
|
+
body: JSON.stringify({
|
|
235
|
+
query: `mutation CreateApiToken($clientId: String!, $input: CreateApiTokenInput!) {
|
|
236
|
+
createClientApiToken(clientId: $clientId, input: $input) { success plainTextToken }
|
|
237
|
+
}`,
|
|
238
|
+
variables: { clientId, input: { name: "openclaw-plugin", role: "agent-events" } },
|
|
239
|
+
}),
|
|
240
|
+
signal: AbortSignal.timeout(10000),
|
|
241
|
+
});
|
|
242
|
+
const data = await res.json();
|
|
243
|
+
return data.data?.createClientApiToken?.plainTextToken || null;
|
|
244
|
+
} catch {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// --- Format helpers ---
|
|
250
|
+
|
|
251
|
+
function formatResults(results) {
|
|
252
|
+
if (!results.length) return "No relevant memories found.";
|
|
253
|
+
return results
|
|
254
|
+
.map((m, i) => `${i + 1}. [${Math.round((m.similarity || 0) * 100)}%] ${m.content}`)
|
|
255
|
+
.join("\n\n");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// --- Plugin entry ---
|
|
259
|
+
|
|
260
|
+
export default {
|
|
261
|
+
id: "pentatonic-memory",
|
|
262
|
+
name: "Pentatonic Memory",
|
|
263
|
+
description: "Persistent, searchable memory with multi-signal retrieval and HyDE query expansion",
|
|
264
|
+
kind: "context-engine",
|
|
265
|
+
|
|
266
|
+
register(api) {
|
|
267
|
+
const config = api.config || {};
|
|
268
|
+
const hosted = !!(config.tes_endpoint && config.tes_api_key);
|
|
269
|
+
const baseUrl = config.memory_url || "http://localhost:3333";
|
|
270
|
+
const searchLimit = config.search_limit || 5;
|
|
271
|
+
const minScore = config.min_score || 0.3;
|
|
272
|
+
const log = (msg) => process.stderr.write(`[pentatonic-memory] ${msg}\n`);
|
|
273
|
+
|
|
274
|
+
stats.mode = hosted ? "hosted" : "local";
|
|
275
|
+
|
|
276
|
+
// Unified search/store that routes to local or hosted
|
|
277
|
+
const search = hosted
|
|
278
|
+
? (query, limit, score) => hostedSearch(config, query, limit, score)
|
|
279
|
+
: (query, limit, score) => localSearch(baseUrl, query, limit, score);
|
|
280
|
+
|
|
281
|
+
const store = hosted
|
|
282
|
+
? (content, metadata) => hostedStore(config, content, metadata)
|
|
283
|
+
: (content, metadata) => localStore(baseUrl, content, metadata);
|
|
284
|
+
|
|
285
|
+
// --- Context engine: always registered, proxies to backend ---
|
|
286
|
+
|
|
287
|
+
api.registerContextEngine("pentatonic-memory", () => ({
|
|
288
|
+
info: {
|
|
289
|
+
id: "pentatonic-memory",
|
|
290
|
+
name: `Pentatonic Memory (${hosted ? "Hosted" : "Local"})`,
|
|
291
|
+
ownsCompaction: false,
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
async ingest({ sessionId, message }) {
|
|
295
|
+
if (!message?.content) return { ingested: false };
|
|
296
|
+
const role = message.role || message.type;
|
|
297
|
+
if (role !== "user" && role !== "assistant") return { ingested: false };
|
|
298
|
+
try {
|
|
299
|
+
await store(message.content, { session_id: sessionId, role });
|
|
300
|
+
stats.memoriesStored++;
|
|
301
|
+
return { ingested: true };
|
|
302
|
+
} catch {
|
|
303
|
+
return { ingested: false };
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
async assemble({ sessionId, messages }) {
|
|
308
|
+
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user" || m.type === "user");
|
|
309
|
+
if (!lastUserMsg?.content) return { messages, estimatedTokens: 0 };
|
|
310
|
+
|
|
311
|
+
// First interaction: check if backend is reachable, prompt setup if not
|
|
312
|
+
if (!stats.setupPrompted && stats.backendReachable === null) {
|
|
313
|
+
if (!hosted) await localHealth(baseUrl);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!stats.setupPrompted && stats.backendReachable === false) {
|
|
317
|
+
stats.setupPrompted = true;
|
|
318
|
+
const setupPrompt = [
|
|
319
|
+
"[Pentatonic Memory] The memory system is installed but not connected to a backend yet.",
|
|
320
|
+
"",
|
|
321
|
+
"Before responding to the user, ask them how they'd like to set up their memory:",
|
|
322
|
+
"",
|
|
323
|
+
"1. **Local** (self-hosted) — Fully private, runs on this machine via Docker. Requires Docker installed.",
|
|
324
|
+
" Use the pentatonic_memory_setup tool with action 'check_local' to verify.",
|
|
325
|
+
"",
|
|
326
|
+
"2. **Hosted** (Pentatonic TES cloud) — Team-wide shared memory, analytics dashboard, higher-dimensional embeddings.",
|
|
327
|
+
" Use the pentatonic_memory_setup tool with action 'setup_hosted' to create an account.",
|
|
328
|
+
"",
|
|
329
|
+
"Ask the user which option they prefer, then use the pentatonic_memory_setup tool to proceed.",
|
|
330
|
+
].join("\n");
|
|
331
|
+
return { messages, estimatedTokens: Math.ceil(setupPrompt.length / 4), systemPromptAddition: setupPrompt };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
const results = await search(lastUserMsg.content, searchLimit, minScore);
|
|
336
|
+
if (!results.length) {
|
|
337
|
+
stats.lastAssembleCount = 0;
|
|
338
|
+
return { messages, estimatedTokens: 0 };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
stats.memoriesInjected += results.length;
|
|
342
|
+
stats.lastAssembleCount = results.length;
|
|
343
|
+
|
|
344
|
+
const memoryText = results
|
|
345
|
+
.map((m) => `- [${Math.round((m.similarity || 0) * 100)}%] ${m.content}`)
|
|
346
|
+
.join("\n");
|
|
347
|
+
|
|
348
|
+
const addition = [
|
|
349
|
+
`[Memory] ${results.length} relevant memories found for this prompt:`,
|
|
350
|
+
memoryText,
|
|
351
|
+
"",
|
|
352
|
+
"When your response is informed by these memories, briefly mention it naturally (e.g. 'From what I remember...' or 'Based on our previous conversations...').",
|
|
353
|
+
].join("\n");
|
|
354
|
+
|
|
355
|
+
return { messages, estimatedTokens: Math.ceil(addition.length / 4), systemPromptAddition: addition };
|
|
356
|
+
} catch {
|
|
357
|
+
stats.lastAssembleCount = 0;
|
|
358
|
+
return { messages, estimatedTokens: 0 };
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
async compact() { return { ok: true, compacted: false }; },
|
|
363
|
+
async afterTurn() {},
|
|
364
|
+
}));
|
|
365
|
+
|
|
366
|
+
// --- Tools ---
|
|
367
|
+
|
|
368
|
+
api.registerTool({
|
|
369
|
+
name: "pentatonic_memory_search",
|
|
370
|
+
description: "Search memories for relevant context. Use when you need to recall past conversations, decisions, or knowledge.",
|
|
371
|
+
parameters: {
|
|
372
|
+
type: "object",
|
|
373
|
+
properties: {
|
|
374
|
+
query: { type: "string", description: "What to search for" },
|
|
375
|
+
limit: { type: "number", description: "Max results (default 5)" },
|
|
376
|
+
},
|
|
377
|
+
required: ["query"],
|
|
378
|
+
},
|
|
379
|
+
async execute({ query, limit }) {
|
|
380
|
+
stats.searchesRun++;
|
|
381
|
+
return formatResults(await search(query, limit || 5, 0.3));
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
api.registerTool({
|
|
386
|
+
name: "pentatonic_memory_store",
|
|
387
|
+
description: "Explicitly store something important. Use for decisions, solutions, or facts worth remembering.",
|
|
388
|
+
parameters: {
|
|
389
|
+
type: "object",
|
|
390
|
+
properties: { content: { type: "string", description: "What to remember" } },
|
|
391
|
+
required: ["content"],
|
|
392
|
+
},
|
|
393
|
+
async execute({ content }) {
|
|
394
|
+
const result = await store(content, { source: "openclaw-tool" });
|
|
395
|
+
if (result) {
|
|
396
|
+
stats.memoriesStored++;
|
|
397
|
+
return `Memory stored. ${randomGif()}`;
|
|
398
|
+
}
|
|
399
|
+
return "Failed to store memory. Is the memory server running?";
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
api.registerTool({
|
|
404
|
+
name: "pentatonic_memory_status",
|
|
405
|
+
description: "Check the status of the Pentatonic Memory system. Shows mode, backend health, and session stats.",
|
|
406
|
+
parameters: { type: "object", properties: {} },
|
|
407
|
+
async execute() {
|
|
408
|
+
// Refresh health check
|
|
409
|
+
if (!hosted) await localHealth(baseUrl);
|
|
410
|
+
|
|
411
|
+
const lines = [
|
|
412
|
+
`**Pentatonic Memory Status**`,
|
|
413
|
+
``,
|
|
414
|
+
`Mode: ${stats.mode}`,
|
|
415
|
+
`Backend: ${hosted ? config.tes_endpoint : baseUrl}`,
|
|
416
|
+
`Status: ${stats.backendReachable ? "connected" : "unreachable"}`,
|
|
417
|
+
``,
|
|
418
|
+
`**Session Stats:**`,
|
|
419
|
+
`Memories injected into prompts: ${stats.memoriesInjected}`,
|
|
420
|
+
`Memories stored: ${stats.memoriesStored}`,
|
|
421
|
+
`Explicit searches: ${stats.searchesRun}`,
|
|
422
|
+
`Last prompt: ${stats.lastAssembleCount} memories used`,
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
if (stats.backendReachable) {
|
|
426
|
+
lines.push("", randomGif());
|
|
427
|
+
} else {
|
|
428
|
+
lines.push("", "Run `npx @pentatonic-ai/ai-agent-sdk memory` to start the local memory server.");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return lines.join("\n");
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
api.registerTool({
|
|
436
|
+
name: "pentatonic_memory_setup",
|
|
437
|
+
description: `Set up or reconfigure Pentatonic Memory. Use this when:
|
|
438
|
+
- The user wants to set up memory for the first time
|
|
439
|
+
- The user wants to switch between local and hosted mode
|
|
440
|
+
- The user wants to connect to Pentatonic TES (hosted cloud memory)
|
|
441
|
+
|
|
442
|
+
Two modes available:
|
|
443
|
+
1. "check_local" — Check if local memory server is running at localhost:3333
|
|
444
|
+
2. "setup_hosted" — Create a Pentatonic TES account and get API credentials. Requires email, client_id (company name), password, and region (EU/US).
|
|
445
|
+
3. "verify_hosted" — After email verification, check if the account is ready. Requires email, password, client_id.
|
|
446
|
+
|
|
447
|
+
For local mode: just check if the server is running. If not, tell the user to run the setup command on their server.
|
|
448
|
+
For hosted mode: walk through account creation step by step via chat.`,
|
|
449
|
+
parameters: {
|
|
450
|
+
type: "object",
|
|
451
|
+
properties: {
|
|
452
|
+
action: {
|
|
453
|
+
type: "string",
|
|
454
|
+
enum: ["check_local", "setup_hosted", "verify_hosted"],
|
|
455
|
+
description: "Which setup action to perform",
|
|
456
|
+
},
|
|
457
|
+
email: { type: "string", description: "User email (hosted only)" },
|
|
458
|
+
client_id: { type: "string", description: "Company/org identifier (hosted only)" },
|
|
459
|
+
password: { type: "string", description: "Account password (hosted only)" },
|
|
460
|
+
region: { type: "string", enum: ["EU", "US"], description: "Data region (hosted only)" },
|
|
461
|
+
},
|
|
462
|
+
required: ["action"],
|
|
463
|
+
},
|
|
464
|
+
async execute({ action, email, client_id, password, region }) {
|
|
465
|
+
if (action === "check_local") {
|
|
466
|
+
const healthy = await localHealth(baseUrl);
|
|
467
|
+
if (healthy) {
|
|
468
|
+
return [
|
|
469
|
+
`Local memory server is running at ${baseUrl}`,
|
|
470
|
+
"",
|
|
471
|
+
"Memory is active. Every conversation is being stored and searched automatically.",
|
|
472
|
+
"",
|
|
473
|
+
randomGif(),
|
|
474
|
+
].join("\n");
|
|
475
|
+
}
|
|
476
|
+
return [
|
|
477
|
+
`Local memory server is not reachable at ${baseUrl}.`,
|
|
478
|
+
"",
|
|
479
|
+
"To start the memory stack, someone needs to run this on the server:",
|
|
480
|
+
"```",
|
|
481
|
+
"npx @pentatonic-ai/ai-agent-sdk memory",
|
|
482
|
+
"```",
|
|
483
|
+
"",
|
|
484
|
+
"This starts PostgreSQL + pgvector, Ollama, and the memory server via Docker.",
|
|
485
|
+
"Once running, memory will activate automatically — no restart needed.",
|
|
486
|
+
].join("\n");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (action === "setup_hosted") {
|
|
490
|
+
if (!email || !client_id || !password) {
|
|
491
|
+
return "I need your email, a client ID (your company name), and a password to create the account. What's your email?";
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Check if already verified
|
|
495
|
+
const existingToken = await tesLogin(email, password, client_id);
|
|
496
|
+
if (existingToken) {
|
|
497
|
+
const apiKey = await tesGetApiKey(existingToken, client_id);
|
|
498
|
+
if (apiKey) {
|
|
499
|
+
const endpoint = `https://${client_id}.api.pentatonic.com`;
|
|
500
|
+
return [
|
|
501
|
+
"Account already verified! Here are your credentials:",
|
|
502
|
+
"",
|
|
503
|
+
`**TES Endpoint:** ${endpoint}`,
|
|
504
|
+
`**Client ID:** ${client_id}`,
|
|
505
|
+
`**API Key:** ${apiKey}`,
|
|
506
|
+
"",
|
|
507
|
+
"Add this to your openclaw.json plugin config to activate hosted memory:",
|
|
508
|
+
"```json",
|
|
509
|
+
JSON.stringify({ tes_endpoint: endpoint, tes_client_id: client_id, tes_api_key: apiKey }, null, 2),
|
|
510
|
+
"```",
|
|
511
|
+
"",
|
|
512
|
+
randomGif(),
|
|
513
|
+
].join("\n");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Enroll
|
|
518
|
+
const result = await tesEnroll(email, password, client_id, region);
|
|
519
|
+
if (result.error) return result.error;
|
|
520
|
+
|
|
521
|
+
return [
|
|
522
|
+
"Account created! Check your email for a verification link.",
|
|
523
|
+
"",
|
|
524
|
+
`Email: ${email}`,
|
|
525
|
+
`Client ID: ${client_id}`,
|
|
526
|
+
"",
|
|
527
|
+
"Once you've clicked the verification link, tell me and I'll finish the setup.",
|
|
528
|
+
].join("\n");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (action === "verify_hosted") {
|
|
532
|
+
if (!email || !client_id || !password) {
|
|
533
|
+
return "I need your email, client ID, and password to check verification status.";
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const token = await tesLogin(email, password, client_id);
|
|
537
|
+
if (!token) {
|
|
538
|
+
return "Account not verified yet. Check your email for the verification link and try again.";
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const apiKey = await tesGetApiKey(token, client_id);
|
|
542
|
+
if (!apiKey) {
|
|
543
|
+
return "Account verified but I couldn't generate an API key. Try again in a moment.";
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const endpoint = `https://${client_id}.api.pentatonic.com`;
|
|
547
|
+
return [
|
|
548
|
+
"Email verified! Here are your credentials:",
|
|
549
|
+
"",
|
|
550
|
+
`**TES Endpoint:** ${endpoint}`,
|
|
551
|
+
`**Client ID:** ${client_id}`,
|
|
552
|
+
`**API Key:** \`${apiKey}\``,
|
|
553
|
+
"",
|
|
554
|
+
"Add this to your openclaw.json plugin config:",
|
|
555
|
+
"```json",
|
|
556
|
+
JSON.stringify({ tes_endpoint: endpoint, tes_client_id: client_id, tes_api_key: apiKey }, null, 2),
|
|
557
|
+
"```",
|
|
558
|
+
"",
|
|
559
|
+
"Then restart the gateway to switch to hosted mode.",
|
|
560
|
+
"",
|
|
561
|
+
randomGif(),
|
|
562
|
+
].join("\n");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return "Unknown action. Use check_local, setup_hosted, or verify_hosted.";
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// Check backend health on startup
|
|
570
|
+
if (!hosted) {
|
|
571
|
+
localHealth(baseUrl).then((ok) => {
|
|
572
|
+
log(ok ? `Memory server healthy at ${baseUrl}` : `Memory server not reachable at ${baseUrl}`);
|
|
573
|
+
});
|
|
574
|
+
} else {
|
|
575
|
+
stats.backendReachable = true; // assume hosted is reachable
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
log(`Plugin registered (${hosted ? "hosted" : "local"} — ${hosted ? config.tes_endpoint : baseUrl})`);
|
|
579
|
+
},
|
|
580
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "pentatonic-memory",
|
|
3
|
+
"name": "Pentatonic Memory",
|
|
4
|
+
"description": "Persistent, searchable memory with multi-signal retrieval and HyDE query expansion. Local (Docker + Ollama) or hosted (Pentatonic TES).",
|
|
5
|
+
"version": "0.4.4",
|
|
6
|
+
"kind": "context-engine",
|
|
7
|
+
"configSchema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"additionalProperties": false,
|
|
10
|
+
"properties": {
|
|
11
|
+
"mode": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"enum": ["local", "hosted"],
|
|
14
|
+
"default": "local",
|
|
15
|
+
"description": "Local (Docker + Ollama) or hosted (Pentatonic TES)"
|
|
16
|
+
},
|
|
17
|
+
"database_url": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "PostgreSQL connection string (local mode)"
|
|
20
|
+
},
|
|
21
|
+
"embedding_url": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "OpenAI-compatible embeddings endpoint (local mode)"
|
|
24
|
+
},
|
|
25
|
+
"embedding_model": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"default": "nomic-embed-text"
|
|
28
|
+
},
|
|
29
|
+
"llm_url": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"description": "OpenAI-compatible chat endpoint for HyDE (local mode)"
|
|
32
|
+
},
|
|
33
|
+
"llm_model": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"default": "llama3.2:3b"
|
|
36
|
+
},
|
|
37
|
+
"tes_endpoint": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "TES API endpoint (hosted mode)"
|
|
40
|
+
},
|
|
41
|
+
"tes_client_id": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"description": "TES client ID (hosted mode)"
|
|
44
|
+
},
|
|
45
|
+
"tes_api_key": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"description": "TES API key (hosted mode)"
|
|
48
|
+
},
|
|
49
|
+
"client_id": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"default": "default",
|
|
52
|
+
"description": "Memory namespace"
|
|
53
|
+
},
|
|
54
|
+
"search_limit": {
|
|
55
|
+
"type": "number",
|
|
56
|
+
"default": 5,
|
|
57
|
+
"description": "Memories to inject per prompt"
|
|
58
|
+
},
|
|
59
|
+
"min_score": {
|
|
60
|
+
"type": "number",
|
|
61
|
+
"default": 0.3,
|
|
62
|
+
"description": "Minimum relevance threshold"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"setup": {
|
|
67
|
+
"description": "Set up persistent memory — local (Docker + Ollama, fully private) or hosted (Pentatonic TES, team-wide)."
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pentatonic-ai/openclaw-memory-plugin",
|
|
3
|
+
"version": "0.5.2",
|
|
4
|
+
"description": "Pentatonic Memory plugin for OpenClaw — persistent, searchable memory with multi-signal retrieval and HyDE query expansion",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"openclaw": {
|
|
8
|
+
"extensions": ["./index.js"],
|
|
9
|
+
"hooks": {
|
|
10
|
+
"context-engine": "pentatonic-memory"
|
|
11
|
+
},
|
|
12
|
+
"compat": {
|
|
13
|
+
"pluginApi": ">=2026.3.7"
|
|
14
|
+
},
|
|
15
|
+
"install": {
|
|
16
|
+
"npmSpec": "@pentatonic-ai/openclaw-memory-plugin"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {},
|
|
20
|
+
"files": [
|
|
21
|
+
"index.js",
|
|
22
|
+
"openclaw.plugin.json"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/Pentatonic-Ltd/ai-agent-sdk.git",
|
|
28
|
+
"directory": "packages/memory/openclaw-plugin"
|
|
29
|
+
},
|
|
30
|
+
"keywords": ["openclaw", "plugin", "memory", "context-engine", "pentatonic", "tes"]
|
|
31
|
+
}
|
|
@@ -33,6 +33,23 @@ export async function search(db, ai, query, opts = {}) {
|
|
|
33
33
|
const threshold = opts.minScore ?? 0.5;
|
|
34
34
|
const w = { ...DEFAULT_WEIGHTS, ...opts.weights };
|
|
35
35
|
|
|
36
|
+
// Check if vector column exists (migration 002 may not have run)
|
|
37
|
+
let hasVectorCol = true;
|
|
38
|
+
try {
|
|
39
|
+
const colCheck = await db(
|
|
40
|
+
`SELECT 1 FROM information_schema.columns
|
|
41
|
+
WHERE table_name = 'memory_nodes' AND column_name = 'embedding_vec' LIMIT 1`,
|
|
42
|
+
[]
|
|
43
|
+
);
|
|
44
|
+
hasVectorCol = (colCheck.rows || []).length > 0;
|
|
45
|
+
} catch {
|
|
46
|
+
hasVectorCol = false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!hasVectorCol) {
|
|
50
|
+
return textSearch(db, query, opts);
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
// Generate query embedding
|
|
37
54
|
const embResult = await ai.embed(query, "query");
|
|
38
55
|
if (!embResult?.embedding) {
|
package/src/transport.js
CHANGED
|
@@ -1,22 +1,42 @@
|
|
|
1
|
-
const
|
|
2
|
-
mutation
|
|
3
|
-
|
|
1
|
+
const CREATE_MODULE_EVENT_MUTATION = `
|
|
2
|
+
mutation CreateModuleEvent($moduleId: String!, $input: ModuleEventInput!) {
|
|
3
|
+
createModuleEvent(moduleId: $moduleId, input: $input) {
|
|
4
4
|
success
|
|
5
5
|
eventId
|
|
6
|
-
message
|
|
7
6
|
}
|
|
8
7
|
}
|
|
9
8
|
`;
|
|
10
9
|
|
|
10
|
+
// Route event types to the correct module
|
|
11
|
+
function getModuleId(eventType) {
|
|
12
|
+
if (["STORE_MEMORY", "SESSION_START", "SESSION_END"].includes(eventType)) {
|
|
13
|
+
return "deep-memory";
|
|
14
|
+
}
|
|
15
|
+
return "conversation-analytics";
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
export async function sendEvent({ endpoint, apiKey, clientId, userId, headers }, input, fetchFn) {
|
|
12
19
|
const f = fetchFn || globalThis.fetch;
|
|
13
20
|
|
|
14
|
-
// tes_ prefixed tokens are API tokens — send as Authorization: Bearer
|
|
15
|
-
// Other tokens (internal service keys) go as x-service-key
|
|
16
21
|
const authHeaders = apiKey.startsWith("tes_")
|
|
17
22
|
? { Authorization: `Bearer ${apiKey}` }
|
|
18
23
|
: { "x-service-key": apiKey };
|
|
19
24
|
|
|
25
|
+
const moduleId = getModuleId(input.eventType);
|
|
26
|
+
const attributes = {
|
|
27
|
+
...input.data?.attributes,
|
|
28
|
+
clientId,
|
|
29
|
+
...(userId ? { userId } : {}),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const moduleInput = {
|
|
33
|
+
eventType: input.eventType,
|
|
34
|
+
data: {
|
|
35
|
+
entity_id: input.data?.entity_id || input.entityId || "",
|
|
36
|
+
attributes,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
20
40
|
const response = await f(`${endpoint}/api/graphql`, {
|
|
21
41
|
method: "POST",
|
|
22
42
|
headers: {
|
|
@@ -26,12 +46,8 @@ export async function sendEvent({ endpoint, apiKey, clientId, userId, headers },
|
|
|
26
46
|
...authHeaders,
|
|
27
47
|
},
|
|
28
48
|
body: JSON.stringify({
|
|
29
|
-
query:
|
|
30
|
-
variables: {
|
|
31
|
-
input: userId
|
|
32
|
-
? { ...input, data: { ...input.data, attributes: { ...input.data?.attributes, userId } } }
|
|
33
|
-
: input,
|
|
34
|
-
},
|
|
49
|
+
query: CREATE_MODULE_EVENT_MUTATION,
|
|
50
|
+
variables: { moduleId, input: moduleInput },
|
|
35
51
|
}),
|
|
36
52
|
});
|
|
37
53
|
|
|
@@ -44,5 +60,5 @@ export async function sendEvent({ endpoint, apiKey, clientId, userId, headers },
|
|
|
44
60
|
throw new Error(`TES GraphQL error: ${json.errors[0].message}`);
|
|
45
61
|
}
|
|
46
62
|
|
|
47
|
-
return json.data.
|
|
63
|
+
return json.data.createModuleEvent;
|
|
48
64
|
}
|