@contextableai/openclaw-memory-graphiti 0.1.1

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/search.ts ADDED
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Parallel Multi-Group Search + Merge/Re-rank
3
+ *
4
+ * Issues parallel search calls to Graphiti (one per authorized group_id),
5
+ * merges results, deduplicates, and re-ranks.
6
+ */
7
+
8
+ import type { GraphitiClient, GraphitiNode, GraphitiFact } from "./graphiti.js";
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ export type SearchResult = {
15
+ type: "node" | "fact";
16
+ uuid: string;
17
+ group_id: string;
18
+ summary: string;
19
+ /** Additional context: entity names for facts, node name for nodes */
20
+ context: string;
21
+ created_at: string;
22
+ };
23
+
24
+ export type SearchOptions = {
25
+ query: string;
26
+ groupIds: string[];
27
+ limit?: number;
28
+ searchNodes?: boolean;
29
+ searchFacts?: boolean;
30
+ };
31
+
32
+ // ============================================================================
33
+ // Search
34
+ // ============================================================================
35
+
36
+ /**
37
+ * Search across multiple authorized group_ids in parallel.
38
+ * Merges and deduplicates results, returning up to `limit` items.
39
+ */
40
+ export async function searchAuthorizedMemories(
41
+ graphiti: GraphitiClient,
42
+ options: SearchOptions,
43
+ ): Promise<SearchResult[]> {
44
+ const { query, groupIds, limit = 10, searchNodes = true, searchFacts = true } = options;
45
+
46
+ if (groupIds.length === 0) {
47
+ return [];
48
+ }
49
+
50
+ // Fan out parallel searches: for each group_id, search nodes and/or facts
51
+ const promises: Promise<SearchResult[]>[] = [];
52
+
53
+ for (const groupId of groupIds) {
54
+ if (searchNodes) {
55
+ promises.push(searchNodesForGroup(graphiti, query, groupId, limit));
56
+ }
57
+ if (searchFacts) {
58
+ promises.push(searchFactsForGroup(graphiti, query, groupId, limit));
59
+ }
60
+ }
61
+
62
+ const resultSets = await Promise.allSettled(promises);
63
+
64
+ // Collect all successful results
65
+ const allResults: SearchResult[] = [];
66
+ for (const result of resultSets) {
67
+ if (result.status === "fulfilled") {
68
+ allResults.push(...result.value);
69
+ }
70
+ // Silently skip failed group searches — partial results are better than none
71
+ }
72
+
73
+ // Deduplicate by UUID
74
+ const seen = new Set<string>();
75
+ const deduped = allResults.filter((r) => {
76
+ if (seen.has(r.uuid)) {
77
+ return false;
78
+ }
79
+ seen.add(r.uuid);
80
+ return true;
81
+ });
82
+
83
+ // Sort by recency (most recent first) and trim to limit
84
+ deduped.sort((a, b) => {
85
+ const dateA = new Date(a.created_at).getTime();
86
+ const dateB = new Date(b.created_at).getTime();
87
+ return dateB - dateA;
88
+ });
89
+
90
+ return deduped.slice(0, limit);
91
+ }
92
+
93
+ // ============================================================================
94
+ // Per-group search helpers
95
+ // ============================================================================
96
+
97
+ async function searchNodesForGroup(
98
+ graphiti: GraphitiClient,
99
+ query: string,
100
+ groupId: string,
101
+ limit: number,
102
+ ): Promise<SearchResult[]> {
103
+ const nodes = await graphiti.searchNodes({ query, group_id: groupId, limit });
104
+ return nodes.map(nodeToResult);
105
+ }
106
+
107
+ async function searchFactsForGroup(
108
+ graphiti: GraphitiClient,
109
+ query: string,
110
+ groupId: string,
111
+ limit: number,
112
+ ): Promise<SearchResult[]> {
113
+ const facts = await graphiti.searchFacts({ query, group_id: groupId, limit });
114
+ return facts.map(factToResult);
115
+ }
116
+
117
+ function nodeToResult(node: GraphitiNode): SearchResult {
118
+ return {
119
+ type: "node",
120
+ uuid: node.uuid,
121
+ group_id: node.group_id,
122
+ summary: node.summary ?? node.name,
123
+ context: node.name,
124
+ created_at: node.created_at ?? new Date().toISOString(),
125
+ };
126
+ }
127
+
128
+ function factToResult(fact: GraphitiFact): SearchResult {
129
+ // Use node names if available, fall back to relationship name or UUIDs
130
+ const source = fact.source_node_name ?? fact.source_node_uuid ?? "?";
131
+ const target = fact.target_node_name ?? fact.target_node_uuid ?? "?";
132
+ const context = fact.name ? `${source} -[${fact.name}]→ ${target}` : `${source} → ${target}`;
133
+ return {
134
+ type: "fact",
135
+ uuid: fact.uuid,
136
+ group_id: fact.group_id,
137
+ summary: fact.fact,
138
+ context,
139
+ created_at: fact.created_at,
140
+ };
141
+ }
142
+
143
+ // ============================================================================
144
+ // Format for agent context
145
+ // ============================================================================
146
+
147
+ /**
148
+ * Format search results into a text block suitable for injecting into agent context.
149
+ */
150
+ export function formatResultsForContext(results: SearchResult[]): string {
151
+ if (results.length === 0) {
152
+ return "";
153
+ }
154
+
155
+ return results
156
+ .map((r, i) => {
157
+ const typeLabel = r.type === "node" ? "entity" : "fact";
158
+ return `${i + 1}. [${typeLabel}] ${r.summary} (${r.context})`;
159
+ })
160
+ .join("\n");
161
+ }
162
+
163
+ /**
164
+ * Format results with session and long-term sections separated.
165
+ * Session group_ids start with "session/".
166
+ */
167
+ export function formatDualResults(
168
+ longTermResults: SearchResult[],
169
+ sessionResults: SearchResult[],
170
+ ): string {
171
+ const parts: string[] = [];
172
+ let idx = 1;
173
+
174
+ if (longTermResults.length > 0) {
175
+ for (const r of longTermResults) {
176
+ const typeLabel = r.type === "node" ? "entity" : "fact";
177
+ parts.push(`${idx++}. [${typeLabel}] ${r.summary} (${r.context})`);
178
+ }
179
+ }
180
+
181
+ if (sessionResults.length > 0) {
182
+ parts.push("Session memories:");
183
+ for (const r of sessionResults) {
184
+ const typeLabel = r.type === "node" ? "entity" : "fact";
185
+ parts.push(`${idx++}. [${typeLabel}] ${r.summary} (${r.context})`);
186
+ }
187
+ }
188
+
189
+ return parts.join("\n");
190
+ }
191
+
192
+ /**
193
+ * Deduplicate session results against long-term results (by UUID).
194
+ */
195
+ export function deduplicateSessionResults(
196
+ longTermResults: SearchResult[],
197
+ sessionResults: SearchResult[],
198
+ ): SearchResult[] {
199
+ const longTermIds = new Set(longTermResults.map((r) => r.uuid));
200
+ return sessionResults.filter((r) => !longTermIds.has(r.uuid));
201
+ }
package/spicedb.ts ADDED
@@ -0,0 +1,174 @@
1
+ /**
2
+ * SpiceDB Client Wrapper
3
+ *
4
+ * Wraps @authzed/authzed-node for authorization operations:
5
+ * WriteSchema, WriteRelationships, LookupResources, CheckPermission.
6
+ */
7
+
8
+ import { v1 } from "@authzed/authzed-node";
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ export type SpiceDbConfig = {
15
+ endpoint: string;
16
+ token: string;
17
+ insecure: boolean;
18
+ };
19
+
20
+ export type RelationshipTuple = {
21
+ resourceType: string;
22
+ resourceId: string;
23
+ relation: string;
24
+ subjectType: string;
25
+ subjectId: string;
26
+ };
27
+
28
+ // ============================================================================
29
+ // Client
30
+ // ============================================================================
31
+
32
+ export class SpiceDbClient {
33
+ private client: ReturnType<typeof v1.NewClient>;
34
+ private promises: ReturnType<typeof v1.NewClient>["promises"];
35
+
36
+ constructor(config: SpiceDbConfig) {
37
+ if (config.insecure) {
38
+ this.client = v1.NewClient(
39
+ config.token,
40
+ config.endpoint,
41
+ v1.ClientSecurity.INSECURE_LOCALHOST_ALLOWED,
42
+ );
43
+ } else {
44
+ this.client = v1.NewClient(config.token, config.endpoint);
45
+ }
46
+ this.promises = this.client.promises;
47
+ }
48
+
49
+ // --------------------------------------------------------------------------
50
+ // Schema
51
+ // --------------------------------------------------------------------------
52
+
53
+ async writeSchema(schema: string): Promise<void> {
54
+ const request = v1.WriteSchemaRequest.create({ schema });
55
+ await this.promises.writeSchema(request);
56
+ }
57
+
58
+ async readSchema(): Promise<string> {
59
+ const request = v1.ReadSchemaRequest.create({});
60
+ const response = await this.promises.readSchema(request);
61
+ return response.schemaText;
62
+ }
63
+
64
+ // --------------------------------------------------------------------------
65
+ // Relationships
66
+ // --------------------------------------------------------------------------
67
+
68
+ async writeRelationships(tuples: RelationshipTuple[]): Promise<void> {
69
+ const updates = tuples.map((t) =>
70
+ v1.RelationshipUpdate.create({
71
+ operation: v1.RelationshipUpdate_Operation.TOUCH,
72
+ relationship: v1.Relationship.create({
73
+ resource: v1.ObjectReference.create({
74
+ objectType: t.resourceType,
75
+ objectId: t.resourceId,
76
+ }),
77
+ relation: t.relation,
78
+ subject: v1.SubjectReference.create({
79
+ object: v1.ObjectReference.create({
80
+ objectType: t.subjectType,
81
+ objectId: t.subjectId,
82
+ }),
83
+ }),
84
+ }),
85
+ }),
86
+ );
87
+
88
+ const request = v1.WriteRelationshipsRequest.create({ updates });
89
+ await this.promises.writeRelationships(request);
90
+ }
91
+
92
+ async deleteRelationships(tuples: RelationshipTuple[]): Promise<void> {
93
+ const updates = tuples.map((t) =>
94
+ v1.RelationshipUpdate.create({
95
+ operation: v1.RelationshipUpdate_Operation.DELETE,
96
+ relationship: v1.Relationship.create({
97
+ resource: v1.ObjectReference.create({
98
+ objectType: t.resourceType,
99
+ objectId: t.resourceId,
100
+ }),
101
+ relation: t.relation,
102
+ subject: v1.SubjectReference.create({
103
+ object: v1.ObjectReference.create({
104
+ objectType: t.subjectType,
105
+ objectId: t.subjectId,
106
+ }),
107
+ }),
108
+ }),
109
+ }),
110
+ );
111
+
112
+ const request = v1.WriteRelationshipsRequest.create({ updates });
113
+ await this.promises.writeRelationships(request);
114
+ }
115
+
116
+ // --------------------------------------------------------------------------
117
+ // Permissions
118
+ // --------------------------------------------------------------------------
119
+
120
+ async checkPermission(params: {
121
+ resourceType: string;
122
+ resourceId: string;
123
+ permission: string;
124
+ subjectType: string;
125
+ subjectId: string;
126
+ }): Promise<boolean> {
127
+ const request = v1.CheckPermissionRequest.create({
128
+ resource: v1.ObjectReference.create({
129
+ objectType: params.resourceType,
130
+ objectId: params.resourceId,
131
+ }),
132
+ permission: params.permission,
133
+ subject: v1.SubjectReference.create({
134
+ object: v1.ObjectReference.create({
135
+ objectType: params.subjectType,
136
+ objectId: params.subjectId,
137
+ }),
138
+ }),
139
+ consistency: v1.Consistency.create({
140
+ requirement: { oneofKind: "fullyConsistent", fullyConsistent: true },
141
+ }),
142
+ });
143
+
144
+ const response = await this.promises.checkPermission(request);
145
+ return (
146
+ response.permissionship ===
147
+ v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION
148
+ );
149
+ }
150
+
151
+ async lookupResources(params: {
152
+ resourceType: string;
153
+ permission: string;
154
+ subjectType: string;
155
+ subjectId: string;
156
+ }): Promise<string[]> {
157
+ const request = v1.LookupResourcesRequest.create({
158
+ resourceObjectType: params.resourceType,
159
+ permission: params.permission,
160
+ subject: v1.SubjectReference.create({
161
+ object: v1.ObjectReference.create({
162
+ objectType: params.subjectType,
163
+ objectId: params.subjectId,
164
+ }),
165
+ }),
166
+ consistency: v1.Consistency.create({
167
+ requirement: { oneofKind: "fullyConsistent", fullyConsistent: true },
168
+ }),
169
+ });
170
+
171
+ const results = await this.promises.lookupResources(request);
172
+ return results.map((r: { resourceObjectId: string }) => r.resourceObjectId);
173
+ }
174
+ }