@contextableai/openclaw-memory-rebac 0.1.0 → 0.1.2
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/dist/authorization.d.ts +57 -0
- package/dist/authorization.js +133 -0
- package/dist/backend.d.ts +135 -0
- package/dist/backend.js +11 -0
- package/dist/backends/graphiti.d.ts +72 -0
- package/dist/backends/graphiti.js +222 -0
- package/dist/backends/registry.d.ts +14 -0
- package/dist/backends/registry.js +12 -0
- package/dist/cli.d.ts +23 -0
- package/dist/cli.js +446 -0
- package/dist/config.d.ts +34 -0
- package/dist/config.js +97 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +638 -0
- package/dist/plugin.defaults.json +12 -0
- package/dist/search.d.ts +34 -0
- package/dist/search.js +98 -0
- package/dist/spicedb.d.ts +80 -0
- package/dist/spicedb.js +267 -0
- package/docker/graphiti/.env +50 -0
- package/docker/graphiti/docker-compose.yml +2 -2
- package/docker/graphiti/graphiti_overlay.py +26 -1
- package/docker/graphiti/startup.py +61 -4
- package/docker/spicedb/.env +14 -0
- package/package.json +8 -11
- package/authorization.ts +0 -191
- package/backend.ts +0 -176
- package/backends/backends.json +0 -3
- package/backends/graphiti.test.ts +0 -292
- package/backends/graphiti.ts +0 -345
- package/backends/registry.ts +0 -36
- package/cli.ts +0 -418
- package/config.ts +0 -141
- package/index.ts +0 -711
- package/search.ts +0 -139
- package/spicedb.ts +0 -355
- /package/{backends → dist/backends}/graphiti.defaults.json +0 -0
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
|
|
3
|
-
vi.mock("node:crypto", async (importOriginal) => {
|
|
4
|
-
const mod = await importOriginal<typeof import("node:crypto")>();
|
|
5
|
-
return { ...mod, randomUUID: vi.fn(() => "test-uuid-000") };
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
import { GraphitiBackend } from "./graphiti.js";
|
|
9
|
-
import type { GraphitiConfig } from "./graphiti.js";
|
|
10
|
-
|
|
11
|
-
const defaultConfig: GraphitiConfig = {
|
|
12
|
-
endpoint: "http://localhost:8000",
|
|
13
|
-
defaultGroupId: "main",
|
|
14
|
-
uuidPollIntervalMs: 100,
|
|
15
|
-
uuidPollMaxAttempts: 5,
|
|
16
|
-
requestTimeoutMs: 5000,
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
// Mock global fetch
|
|
20
|
-
const mockFetch = vi.fn();
|
|
21
|
-
global.fetch = mockFetch as unknown as typeof fetch;
|
|
22
|
-
|
|
23
|
-
/** Helper to create a mock JSON response */
|
|
24
|
-
function jsonResponse(body: unknown, status = 200) {
|
|
25
|
-
return {
|
|
26
|
-
ok: status >= 200 && status < 300,
|
|
27
|
-
status,
|
|
28
|
-
headers: new Map([["content-type", "application/json"]]),
|
|
29
|
-
json: vi.fn().mockResolvedValue(body),
|
|
30
|
-
text: vi.fn().mockResolvedValue(JSON.stringify(body)),
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
describe("GraphitiBackend", () => {
|
|
35
|
-
beforeEach(() => {
|
|
36
|
-
mockFetch.mockReset();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
afterEach(() => {
|
|
40
|
-
vi.restoreAllMocks();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("has correct name", () => {
|
|
44
|
-
const backend = new GraphitiBackend(defaultConfig);
|
|
45
|
-
expect(backend.name).toBe("graphiti");
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe("store", () => {
|
|
49
|
-
test("returns StoreResult with fragmentId Promise", async () => {
|
|
50
|
-
const backend = new GraphitiBackend(defaultConfig);
|
|
51
|
-
|
|
52
|
-
// Mock POST /messages (202 Accepted)
|
|
53
|
-
mockFetch.mockResolvedValueOnce(
|
|
54
|
-
jsonResponse({ message: "Messages accepted", success: true }, 202),
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
// Mock GET /episodes polling (return episode on first poll)
|
|
58
|
-
mockFetch.mockResolvedValueOnce(
|
|
59
|
-
jsonResponse([
|
|
60
|
-
{ uuid: "real-uuid-123", name: "memory_test-uuid-000", group_id: "main", created_at: "2026-03-01" },
|
|
61
|
-
]),
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
const result = await backend.store({
|
|
65
|
-
content: "Test memory",
|
|
66
|
-
groupId: "main",
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
expect(result.fragmentId).toBeInstanceOf(Promise);
|
|
70
|
-
const uuid = await result.fragmentId;
|
|
71
|
-
expect(uuid).toBe("real-uuid-123");
|
|
72
|
-
|
|
73
|
-
// Verify POST /messages was called
|
|
74
|
-
const storeCall = mockFetch.mock.calls[0];
|
|
75
|
-
expect(storeCall[0]).toBe("http://localhost:8000/messages");
|
|
76
|
-
const body = JSON.parse(storeCall[1].body as string);
|
|
77
|
-
expect(body.group_id).toBe("main");
|
|
78
|
-
expect(body.messages).toHaveLength(1);
|
|
79
|
-
expect(body.messages[0].content).toBe("Test memory");
|
|
80
|
-
expect(body.messages[0].uuid).toBeUndefined();
|
|
81
|
-
expect(body.messages[0].name).toBe("memory_test-uuid-000");
|
|
82
|
-
expect(body.messages[0].timestamp).toBeDefined();
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test("passes customPrompt in message content with instructions wrapper", async () => {
|
|
86
|
-
const backend = new GraphitiBackend(defaultConfig);
|
|
87
|
-
|
|
88
|
-
// Mock POST /messages
|
|
89
|
-
mockFetch.mockResolvedValueOnce(
|
|
90
|
-
jsonResponse({ message: "Accepted", success: true }, 202),
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
await backend.store({
|
|
94
|
-
content: "Important fact",
|
|
95
|
-
groupId: "main",
|
|
96
|
-
customPrompt: "Extract only names",
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
const storeCall = mockFetch.mock.calls[0];
|
|
100
|
-
const body = JSON.parse(storeCall[1].body as string);
|
|
101
|
-
expect(body.messages[0].content).toContain("[Extraction Instructions]");
|
|
102
|
-
expect(body.messages[0].content).toContain("Extract only names");
|
|
103
|
-
expect(body.messages[0].content).toContain("[End Instructions]");
|
|
104
|
-
expect(body.messages[0].content).toContain("Important fact");
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
describe("searchGroup", () => {
|
|
109
|
-
test("calls POST /search and returns facts as SearchResult[]", async () => {
|
|
110
|
-
const backend = new GraphitiBackend(defaultConfig);
|
|
111
|
-
|
|
112
|
-
mockFetch.mockResolvedValueOnce(
|
|
113
|
-
jsonResponse({
|
|
114
|
-
facts: [
|
|
115
|
-
{
|
|
116
|
-
uuid: "f1",
|
|
117
|
-
name: "WORKS_AT",
|
|
118
|
-
fact: "Mark works at Acme",
|
|
119
|
-
valid_at: null,
|
|
120
|
-
invalid_at: null,
|
|
121
|
-
created_at: "2026-01-16",
|
|
122
|
-
expired_at: null,
|
|
123
|
-
},
|
|
124
|
-
],
|
|
125
|
-
}),
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
const results = await backend.searchGroup({
|
|
129
|
-
query: "Mark work",
|
|
130
|
-
groupId: "g1",
|
|
131
|
-
limit: 10,
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
expect(results).toHaveLength(1);
|
|
135
|
-
expect(results[0]).toMatchObject({
|
|
136
|
-
type: "fact",
|
|
137
|
-
uuid: "f1",
|
|
138
|
-
group_id: "g1",
|
|
139
|
-
summary: "Mark works at Acme",
|
|
140
|
-
context: "WORKS_AT",
|
|
141
|
-
created_at: "2026-01-16",
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// Verify POST /search was called with correct body
|
|
145
|
-
const searchCall = mockFetch.mock.calls[0];
|
|
146
|
-
expect(searchCall[0]).toBe("http://localhost:8000/search");
|
|
147
|
-
const body = JSON.parse(searchCall[1].body as string);
|
|
148
|
-
expect(body.group_ids).toEqual(["g1"]);
|
|
149
|
-
expect(body.query).toBe("Mark work");
|
|
150
|
-
expect(body.max_facts).toBe(10);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
test("returns empty array when no facts found", async () => {
|
|
154
|
-
const backend = new GraphitiBackend(defaultConfig);
|
|
155
|
-
|
|
156
|
-
mockFetch.mockResolvedValueOnce(jsonResponse({ facts: [] }));
|
|
157
|
-
|
|
158
|
-
const results = await backend.searchGroup({
|
|
159
|
-
query: "nothing",
|
|
160
|
-
groupId: "g1",
|
|
161
|
-
limit: 10,
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
expect(results).toEqual([]);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
describe("healthCheck", () => {
|
|
169
|
-
test("returns true when /healthcheck responds ok", async () => {
|
|
170
|
-
const backend = new GraphitiBackend(defaultConfig);
|
|
171
|
-
mockFetch.mockResolvedValueOnce({ ok: true });
|
|
172
|
-
|
|
173
|
-
const healthy = await backend.healthCheck();
|
|
174
|
-
expect(healthy).toBe(true);
|
|
175
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
176
|
-
"http://localhost:8000/healthcheck",
|
|
177
|
-
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
|
178
|
-
);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("returns false when /healthcheck fails", async () => {
|
|
182
|
-
const backend = new GraphitiBackend(defaultConfig);
|
|
183
|
-
mockFetch.mockResolvedValueOnce({ ok: false });
|
|
184
|
-
|
|
185
|
-
const healthy = await backend.healthCheck();
|
|
186
|
-
expect(healthy).toBe(false);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test("returns false when /healthcheck throws", async () => {
|
|
190
|
-
const backend = new GraphitiBackend(defaultConfig);
|
|
191
|
-
mockFetch.mockRejectedValueOnce(new Error("Network error"));
|
|
192
|
-
|
|
193
|
-
const healthy = await backend.healthCheck();
|
|
194
|
-
expect(healthy).toBe(false);
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
describe("getStatus", () => {
|
|
199
|
-
test("returns status object with backend name and healthy flag", async () => {
|
|
200
|
-
const backend = new GraphitiBackend(defaultConfig);
|
|
201
|
-
mockFetch.mockResolvedValueOnce({ ok: true });
|
|
202
|
-
|
|
203
|
-
const status = await backend.getStatus();
|
|
204
|
-
expect(status.backend).toBe("graphiti");
|
|
205
|
-
expect(status.endpoint).toBe("http://localhost:8000");
|
|
206
|
-
expect(status.healthy).toBe(true);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test("reports unhealthy when health check fails", async () => {
|
|
210
|
-
const backend = new GraphitiBackend(defaultConfig);
|
|
211
|
-
mockFetch.mockRejectedValueOnce(new Error("Down"));
|
|
212
|
-
|
|
213
|
-
const status = await backend.getStatus();
|
|
214
|
-
expect(status.healthy).toBe(false);
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
describe("deleteFragment", () => {
|
|
219
|
-
test("calls DELETE /episode/{uuid}", async () => {
|
|
220
|
-
const backend = new GraphitiBackend(defaultConfig);
|
|
221
|
-
|
|
222
|
-
mockFetch.mockResolvedValueOnce(
|
|
223
|
-
jsonResponse({ message: "Deleted", success: true }),
|
|
224
|
-
);
|
|
225
|
-
|
|
226
|
-
const result = await backend.deleteFragment?.("episode-uuid-123");
|
|
227
|
-
expect(result).toBe(true);
|
|
228
|
-
|
|
229
|
-
const deleteCall = mockFetch.mock.calls[0];
|
|
230
|
-
expect(deleteCall[0]).toBe("http://localhost:8000/episode/episode-uuid-123");
|
|
231
|
-
expect(deleteCall[1].method).toBe("DELETE");
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
describe("deleteGroup", () => {
|
|
236
|
-
test("calls DELETE /group/{groupId}", async () => {
|
|
237
|
-
const backend = new GraphitiBackend(defaultConfig);
|
|
238
|
-
|
|
239
|
-
mockFetch.mockResolvedValueOnce(
|
|
240
|
-
jsonResponse({ message: "Cleared", success: true }),
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
await backend.deleteGroup("old-group");
|
|
244
|
-
|
|
245
|
-
const deleteCall = mockFetch.mock.calls[0];
|
|
246
|
-
expect(deleteCall[0]).toBe("http://localhost:8000/group/old-group");
|
|
247
|
-
expect(deleteCall[1].method).toBe("DELETE");
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
describe("listGroups", () => {
|
|
252
|
-
test("returns empty array (not implemented)", async () => {
|
|
253
|
-
const backend = new GraphitiBackend(defaultConfig);
|
|
254
|
-
const groups = await backend.listGroups();
|
|
255
|
-
expect(groups).toEqual([]);
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
describe("getConversationHistory", () => {
|
|
260
|
-
test("maps getEpisodes to ConversationTurn[]", async () => {
|
|
261
|
-
const backend = new GraphitiBackend(defaultConfig);
|
|
262
|
-
|
|
263
|
-
mockFetch.mockResolvedValueOnce(
|
|
264
|
-
jsonResponse([
|
|
265
|
-
{ uuid: "e1", name: "Turn 1", content: "User: Hi\nAssistant: Hello", created_at: "2026-01-15" },
|
|
266
|
-
{ uuid: "e2", name: "Turn 2", content: "User: Bye\nAssistant: Goodbye", created_at: "2026-01-16" },
|
|
267
|
-
]),
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
const turns = await backend.getConversationHistory("session-123", 10);
|
|
271
|
-
expect(turns).toHaveLength(2);
|
|
272
|
-
expect(turns[0]).toMatchObject({
|
|
273
|
-
query: "Turn 1",
|
|
274
|
-
answer: "User: Hi\nAssistant: Hello",
|
|
275
|
-
created_at: "2026-01-15",
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
// Verify GET /episodes/session-session-123 was called
|
|
279
|
-
const episodesCall = mockFetch.mock.calls[0];
|
|
280
|
-
expect(episodesCall[0]).toBe("http://localhost:8000/episodes/session-session-123?last_n=10");
|
|
281
|
-
expect(episodesCall[1].method).toBe("GET");
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test("returns empty array on error", async () => {
|
|
285
|
-
const backend = new GraphitiBackend(defaultConfig);
|
|
286
|
-
mockFetch.mockRejectedValueOnce(new Error("Fail"));
|
|
287
|
-
|
|
288
|
-
const turns = await backend.getConversationHistory("session-123");
|
|
289
|
-
expect(turns).toEqual([]);
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
});
|
package/backends/graphiti.ts
DELETED
|
@@ -1,345 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GraphitiBackend — MemoryBackend implementation backed by the Graphiti FastAPI REST server.
|
|
3
|
-
*
|
|
4
|
-
* Graphiti communicates via standard HTTP REST endpoints.
|
|
5
|
-
* Episodes are processed asynchronously by Graphiti's LLM pipeline;
|
|
6
|
-
* the real server-side UUID is discovered by polling GET /episodes/{group_id}.
|
|
7
|
-
*
|
|
8
|
-
* store() returns immediately; fragmentId resolves once Graphiti finishes
|
|
9
|
-
* processing and the UUID becomes visible in the episodes list.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { randomUUID } from "node:crypto";
|
|
13
|
-
import type { Command } from "commander";
|
|
14
|
-
import type {
|
|
15
|
-
MemoryBackend,
|
|
16
|
-
SearchResult,
|
|
17
|
-
StoreResult,
|
|
18
|
-
ConversationTurn,
|
|
19
|
-
BackendDataset,
|
|
20
|
-
} from "../backend.js";
|
|
21
|
-
|
|
22
|
-
// ============================================================================
|
|
23
|
-
// Types (Graphiti REST API)
|
|
24
|
-
// ============================================================================
|
|
25
|
-
|
|
26
|
-
/** Matches the server's Message schema (from /openapi.json). */
|
|
27
|
-
type GraphitiMessage = {
|
|
28
|
-
content: string;
|
|
29
|
-
role_type: "user" | "assistant" | "system";
|
|
30
|
-
role: string | null;
|
|
31
|
-
name?: string;
|
|
32
|
-
timestamp?: string;
|
|
33
|
-
source_description?: string;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
type AddMessagesRequest = {
|
|
37
|
-
group_id: string;
|
|
38
|
-
messages: GraphitiMessage[];
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
type GraphitiEpisode = {
|
|
42
|
-
uuid: string;
|
|
43
|
-
name: string;
|
|
44
|
-
content: string;
|
|
45
|
-
source_description: string;
|
|
46
|
-
group_id: string;
|
|
47
|
-
created_at: string;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
type FactResult = {
|
|
51
|
-
uuid: string;
|
|
52
|
-
name: string;
|
|
53
|
-
fact: string;
|
|
54
|
-
valid_at: string | null;
|
|
55
|
-
invalid_at: string | null;
|
|
56
|
-
created_at: string;
|
|
57
|
-
expired_at: string | null;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
type SearchRequest = {
|
|
61
|
-
group_ids: string[];
|
|
62
|
-
query: string;
|
|
63
|
-
max_facts?: number;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
type SearchResults = {
|
|
67
|
-
facts: FactResult[];
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
type GraphitiResult = {
|
|
71
|
-
message: string;
|
|
72
|
-
success: boolean;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// ============================================================================
|
|
76
|
-
// GraphitiBackend
|
|
77
|
-
// ============================================================================
|
|
78
|
-
|
|
79
|
-
export type GraphitiConfig = {
|
|
80
|
-
endpoint: string;
|
|
81
|
-
defaultGroupId: string;
|
|
82
|
-
uuidPollIntervalMs: number;
|
|
83
|
-
uuidPollMaxAttempts: number;
|
|
84
|
-
requestTimeoutMs?: number;
|
|
85
|
-
customInstructions: string;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export class GraphitiBackend implements MemoryBackend {
|
|
89
|
-
readonly name = "graphiti";
|
|
90
|
-
|
|
91
|
-
readonly uuidPollIntervalMs: number;
|
|
92
|
-
readonly uuidPollMaxAttempts: number;
|
|
93
|
-
private readonly requestTimeoutMs: number;
|
|
94
|
-
|
|
95
|
-
constructor(private readonly config: GraphitiConfig) {
|
|
96
|
-
this.uuidPollIntervalMs = config.uuidPollIntervalMs;
|
|
97
|
-
this.uuidPollMaxAttempts = config.uuidPollMaxAttempts;
|
|
98
|
-
this.requestTimeoutMs = config.requestTimeoutMs ?? 30000;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// --------------------------------------------------------------------------
|
|
102
|
-
// REST transport
|
|
103
|
-
// --------------------------------------------------------------------------
|
|
104
|
-
|
|
105
|
-
private async restCall<T>(
|
|
106
|
-
method: "GET" | "POST" | "DELETE",
|
|
107
|
-
path: string,
|
|
108
|
-
body?: unknown,
|
|
109
|
-
): Promise<T> {
|
|
110
|
-
const url = `${this.config.endpoint}${path}`;
|
|
111
|
-
const opts: RequestInit = {
|
|
112
|
-
method,
|
|
113
|
-
headers: { "Content-Type": "application/json" },
|
|
114
|
-
signal: AbortSignal.timeout(this.requestTimeoutMs),
|
|
115
|
-
};
|
|
116
|
-
if (body !== undefined) {
|
|
117
|
-
opts.body = JSON.stringify(body);
|
|
118
|
-
}
|
|
119
|
-
const response = await fetch(url, opts);
|
|
120
|
-
if (!response.ok) {
|
|
121
|
-
const text = await response.text().catch(() => "");
|
|
122
|
-
throw new Error(`Graphiti REST ${method} ${path} failed: ${response.status} ${text}`);
|
|
123
|
-
}
|
|
124
|
-
const ct = response.headers.get("content-type") ?? "";
|
|
125
|
-
if (ct.includes("application/json")) {
|
|
126
|
-
return (await response.json()) as T;
|
|
127
|
-
}
|
|
128
|
-
return {} as T;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// --------------------------------------------------------------------------
|
|
132
|
-
// MemoryBackend implementation
|
|
133
|
-
// --------------------------------------------------------------------------
|
|
134
|
-
|
|
135
|
-
async store(params: {
|
|
136
|
-
content: string;
|
|
137
|
-
groupId: string;
|
|
138
|
-
sourceDescription?: string;
|
|
139
|
-
customPrompt?: string;
|
|
140
|
-
}): Promise<StoreResult> {
|
|
141
|
-
const episodeName = `memory_${randomUUID()}`;
|
|
142
|
-
let effectiveBody = params.content;
|
|
143
|
-
if (params.customPrompt) {
|
|
144
|
-
effectiveBody = `[Extraction Instructions]\n${params.customPrompt}\n[End Instructions]\n\n${params.content}`;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const request: AddMessagesRequest = {
|
|
148
|
-
group_id: params.groupId,
|
|
149
|
-
messages: [
|
|
150
|
-
{
|
|
151
|
-
name: episodeName,
|
|
152
|
-
content: effectiveBody,
|
|
153
|
-
timestamp: new Date().toISOString(),
|
|
154
|
-
role_type: "user",
|
|
155
|
-
role: "user",
|
|
156
|
-
source_description: params.sourceDescription,
|
|
157
|
-
},
|
|
158
|
-
],
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
await this.restCall<GraphitiResult>("POST", "/messages", request);
|
|
162
|
-
|
|
163
|
-
// POST /messages returns 202 (async processing).
|
|
164
|
-
// Poll GET /episodes until the episode appears, then return its real UUID.
|
|
165
|
-
const fragmentId = this.resolveEpisodeUuid(episodeName, params.groupId);
|
|
166
|
-
fragmentId.catch(() => {}); // Prevent unhandled rejection if caller drops it
|
|
167
|
-
|
|
168
|
-
return { fragmentId };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
private async resolveEpisodeUuid(name: string, groupId: string): Promise<string> {
|
|
172
|
-
for (let i = 0; i < this.uuidPollMaxAttempts; i++) {
|
|
173
|
-
await new Promise((r) => setTimeout(r, this.uuidPollIntervalMs));
|
|
174
|
-
try {
|
|
175
|
-
const episodes = await this.getEpisodes(groupId, 50);
|
|
176
|
-
const match = episodes.find((ep) => ep.name === name);
|
|
177
|
-
if (match) return match.uuid;
|
|
178
|
-
} catch {
|
|
179
|
-
// Transient error — keep polling
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
throw new Error(`Timed out resolving episode UUID for "${name}" in group "${groupId}"`);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async searchGroup(params: {
|
|
186
|
-
query: string;
|
|
187
|
-
groupId: string;
|
|
188
|
-
limit: number;
|
|
189
|
-
sessionId?: string;
|
|
190
|
-
}): Promise<SearchResult[]> {
|
|
191
|
-
const { query, groupId, limit } = params;
|
|
192
|
-
|
|
193
|
-
const searchRequest: SearchRequest = {
|
|
194
|
-
group_ids: [groupId],
|
|
195
|
-
query,
|
|
196
|
-
max_facts: limit,
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
const response = await this.restCall<SearchResults>("POST", "/search", searchRequest);
|
|
200
|
-
const facts = response.facts ?? [];
|
|
201
|
-
|
|
202
|
-
return facts.map((f) => ({
|
|
203
|
-
type: "fact" as const,
|
|
204
|
-
uuid: f.uuid,
|
|
205
|
-
group_id: groupId,
|
|
206
|
-
summary: f.fact,
|
|
207
|
-
context: f.name,
|
|
208
|
-
created_at: f.created_at,
|
|
209
|
-
}));
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async getConversationHistory(sessionId: string, lastN = 10): Promise<ConversationTurn[]> {
|
|
213
|
-
const sessionGroup = `session-${sessionId.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
|
214
|
-
try {
|
|
215
|
-
const episodes = await this.getEpisodes(sessionGroup, lastN);
|
|
216
|
-
return episodes.map((ep) => ({
|
|
217
|
-
query: ep.name,
|
|
218
|
-
answer: ep.content,
|
|
219
|
-
created_at: ep.created_at,
|
|
220
|
-
}));
|
|
221
|
-
} catch {
|
|
222
|
-
return [];
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async healthCheck(): Promise<boolean> {
|
|
227
|
-
try {
|
|
228
|
-
const response = await fetch(`${this.config.endpoint}/healthcheck`, {
|
|
229
|
-
signal: AbortSignal.timeout(5000),
|
|
230
|
-
});
|
|
231
|
-
return response.ok;
|
|
232
|
-
} catch {
|
|
233
|
-
return false;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
async getStatus(): Promise<Record<string, unknown>> {
|
|
238
|
-
return {
|
|
239
|
-
backend: "graphiti",
|
|
240
|
-
endpoint: this.config.endpoint,
|
|
241
|
-
healthy: await this.healthCheck(),
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async deleteGroup(groupId: string): Promise<void> {
|
|
246
|
-
await this.restCall<GraphitiResult>(
|
|
247
|
-
"DELETE",
|
|
248
|
-
`/group/${encodeURIComponent(groupId)}`,
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
async listGroups(): Promise<BackendDataset[]> {
|
|
253
|
-
// Graphiti has no list-groups API; the CLI can query SpiceDB for this
|
|
254
|
-
return [];
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async deleteFragment(uuid: string): Promise<boolean> {
|
|
258
|
-
await this.restCall<GraphitiResult>(
|
|
259
|
-
"DELETE",
|
|
260
|
-
`/episode/${encodeURIComponent(uuid)}`,
|
|
261
|
-
);
|
|
262
|
-
return true;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// --------------------------------------------------------------------------
|
|
266
|
-
// Graphiti-specific helpers (used by CLI commands and UUID polling)
|
|
267
|
-
// --------------------------------------------------------------------------
|
|
268
|
-
|
|
269
|
-
async getEpisodes(groupId: string, lastN: number): Promise<GraphitiEpisode[]> {
|
|
270
|
-
return this.restCall<GraphitiEpisode[]>(
|
|
271
|
-
"GET",
|
|
272
|
-
`/episodes/${encodeURIComponent(groupId)}?last_n=${lastN}`,
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
async getEntityEdge(uuid: string): Promise<FactResult> {
|
|
277
|
-
return this.restCall<FactResult>(
|
|
278
|
-
"GET",
|
|
279
|
-
`/entity-edge/${encodeURIComponent(uuid)}`,
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// --------------------------------------------------------------------------
|
|
284
|
-
// Backend-specific CLI commands
|
|
285
|
-
// --------------------------------------------------------------------------
|
|
286
|
-
|
|
287
|
-
registerCliCommands(cmd: Command): void {
|
|
288
|
-
cmd
|
|
289
|
-
.command("episodes")
|
|
290
|
-
.description("[graphiti] List recent episodes for a group")
|
|
291
|
-
.option("--last <n>", "Number of episodes", "10")
|
|
292
|
-
.option("--group <id>", "Group ID")
|
|
293
|
-
.action(async (opts: { last: string; group?: string }) => {
|
|
294
|
-
const groupId = opts.group ?? this.config.defaultGroupId;
|
|
295
|
-
const episodes = await this.getEpisodes(groupId, parseInt(opts.last));
|
|
296
|
-
console.log(JSON.stringify(episodes, null, 2));
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
cmd
|
|
300
|
-
.command("fact")
|
|
301
|
-
.description("[graphiti] Get a specific fact (entity edge) by UUID")
|
|
302
|
-
.argument("<uuid>", "Fact UUID")
|
|
303
|
-
.action(async (uuid: string) => {
|
|
304
|
-
try {
|
|
305
|
-
const fact = await this.getEntityEdge(uuid);
|
|
306
|
-
console.log(JSON.stringify(fact, null, 2));
|
|
307
|
-
} catch (err) {
|
|
308
|
-
console.error(`Failed to get fact: ${err instanceof Error ? err.message : String(err)}`);
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
cmd
|
|
313
|
-
.command("clear-graph")
|
|
314
|
-
.description("[graphiti] Clear graph data for a group (destructive!)")
|
|
315
|
-
.option("--group <id...>", "Group ID(s)")
|
|
316
|
-
.option("--confirm", "Required safety flag", false)
|
|
317
|
-
.action(async (opts: { group?: string[]; confirm: boolean }) => {
|
|
318
|
-
if (!opts.confirm) {
|
|
319
|
-
console.log("Destructive operation. Pass --confirm to proceed.");
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
const groups = opts.group ?? [];
|
|
323
|
-
if (groups.length === 0) {
|
|
324
|
-
console.log("No groups specified. Use --group <id> to specify groups.");
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
for (const g of groups) {
|
|
328
|
-
await this.deleteGroup(g);
|
|
329
|
-
console.log(`Cleared group: ${g}`);
|
|
330
|
-
}
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// ============================================================================
|
|
336
|
-
// Backend module exports (used by backends/registry.ts)
|
|
337
|
-
// ============================================================================
|
|
338
|
-
|
|
339
|
-
import graphitiDefaults from "./graphiti.defaults.json" with { type: "json" };
|
|
340
|
-
|
|
341
|
-
export const defaults: Record<string, unknown> = graphitiDefaults;
|
|
342
|
-
|
|
343
|
-
export function create(config: Record<string, unknown>): MemoryBackend {
|
|
344
|
-
return new GraphitiBackend(config as GraphitiConfig);
|
|
345
|
-
}
|
package/backends/registry.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Backend registry — loaded dynamically from backends.json.
|
|
3
|
-
*
|
|
4
|
-
* Call initRegistry() once (e.g. at the start of register()) before using
|
|
5
|
-
* backendRegistry or createBackend(). No backend names appear in this file.
|
|
6
|
-
*
|
|
7
|
-
* To add a new backend:
|
|
8
|
-
* 1. Create backends/<name>.ts (exports `defaults` and `create`)
|
|
9
|
-
* 2. Create backends/<name>.defaults.json
|
|
10
|
-
* 3. Add `"<name>": "./<name>.js"` to backends/backends.json
|
|
11
|
-
* No TypeScript changes needed anywhere else.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import backendsJson from "./backends.json" with { type: "json" };
|
|
15
|
-
import type { MemoryBackend } from "../backend.js";
|
|
16
|
-
|
|
17
|
-
export type BackendModule = {
|
|
18
|
-
create: (config: Record<string, unknown>) => MemoryBackend;
|
|
19
|
-
defaults: Record<string, unknown>;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
// Mutable backing store — populated by initRegistry().
|
|
23
|
-
// backendRegistry is a live reference to the same object.
|
|
24
|
-
const _registry: Record<string, BackendModule> = {};
|
|
25
|
-
|
|
26
|
-
export async function initRegistry(): Promise<void> {
|
|
27
|
-
if (Object.keys(_registry).length > 0) return;
|
|
28
|
-
for (const [name, modulePath] of Object.entries(backendsJson as Record<string, string>)) {
|
|
29
|
-
const url = new URL(modulePath, import.meta.url);
|
|
30
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
-
const mod = await import(url.href) as any;
|
|
32
|
-
_registry[name] = mod as BackendModule;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export const backendRegistry: Readonly<Record<string, BackendModule>> = _registry;
|