mem0ai 2.0.2 → 2.1.0
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/oss/index.d.mts +100 -13
- package/dist/oss/index.d.ts +100 -13
- package/dist/oss/index.js +853 -33
- package/dist/oss/index.js.map +1 -1
- package/dist/oss/index.mjs +853 -33
- package/dist/oss/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/oss/index.js
CHANGED
|
@@ -77,8 +77,19 @@ var MemoryConfigSchema = import_zod.z.object({
|
|
|
77
77
|
}),
|
|
78
78
|
historyDbPath: import_zod.z.string().optional(),
|
|
79
79
|
customPrompt: import_zod.z.string().optional(),
|
|
80
|
+
enableGraph: import_zod.z.boolean().optional(),
|
|
80
81
|
graphStore: import_zod.z.object({
|
|
81
|
-
|
|
82
|
+
provider: import_zod.z.string(),
|
|
83
|
+
config: import_zod.z.object({
|
|
84
|
+
url: import_zod.z.string(),
|
|
85
|
+
username: import_zod.z.string(),
|
|
86
|
+
password: import_zod.z.string()
|
|
87
|
+
}),
|
|
88
|
+
llm: import_zod.z.object({
|
|
89
|
+
provider: import_zod.z.string(),
|
|
90
|
+
config: import_zod.z.record(import_zod.z.string(), import_zod.z.any())
|
|
91
|
+
}).optional(),
|
|
92
|
+
customPrompt: import_zod.z.string().optional()
|
|
82
93
|
}).optional()
|
|
83
94
|
});
|
|
84
95
|
|
|
@@ -112,16 +123,28 @@ var OpenAILLM = class {
|
|
|
112
123
|
this.openai = new import_openai2.default({ apiKey: config.apiKey });
|
|
113
124
|
this.model = config.model || "gpt-4-turbo-preview";
|
|
114
125
|
}
|
|
115
|
-
async generateResponse(messages, responseFormat) {
|
|
126
|
+
async generateResponse(messages, responseFormat, tools) {
|
|
116
127
|
const completion = await this.openai.chat.completions.create({
|
|
117
128
|
messages: messages.map((msg) => ({
|
|
118
129
|
role: msg.role,
|
|
119
130
|
content: msg.content
|
|
120
131
|
})),
|
|
121
132
|
model: this.model,
|
|
122
|
-
response_format: responseFormat
|
|
133
|
+
response_format: responseFormat,
|
|
134
|
+
...tools && { tools, tool_choice: "auto" }
|
|
123
135
|
});
|
|
124
|
-
|
|
136
|
+
const response = completion.choices[0].message;
|
|
137
|
+
if (response.tool_calls) {
|
|
138
|
+
return {
|
|
139
|
+
content: response.content || "",
|
|
140
|
+
role: response.role,
|
|
141
|
+
toolCalls: response.tool_calls.map((call) => ({
|
|
142
|
+
name: call.function.name,
|
|
143
|
+
arguments: call.function.arguments
|
|
144
|
+
}))
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return response.content || "";
|
|
125
148
|
}
|
|
126
149
|
async generateChat(messages) {
|
|
127
150
|
const completion = await this.openai.chat.completions.create({
|
|
@@ -143,37 +166,57 @@ var OpenAILLM = class {
|
|
|
143
166
|
var import_openai3 = __toESM(require("openai"));
|
|
144
167
|
var OpenAIStructuredLLM = class {
|
|
145
168
|
constructor(config) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
throw new Error("OpenAI API key is required");
|
|
149
|
-
}
|
|
150
|
-
const baseUrl = process.env.OPENAI_API_BASE || "https://api.openai.com/v1";
|
|
151
|
-
this.client = new import_openai3.default({ apiKey, baseURL: baseUrl });
|
|
152
|
-
this.model = config.model || "gpt-4-0125-preview";
|
|
169
|
+
this.openai = new import_openai3.default({ apiKey: config.apiKey });
|
|
170
|
+
this.model = config.model || "gpt-4-turbo-preview";
|
|
153
171
|
}
|
|
154
|
-
async generateResponse(messages, responseFormat) {
|
|
155
|
-
const
|
|
156
|
-
model: this.model,
|
|
172
|
+
async generateResponse(messages, responseFormat, tools) {
|
|
173
|
+
const completion = await this.openai.chat.completions.create({
|
|
157
174
|
messages: messages.map((msg) => ({
|
|
158
175
|
role: msg.role,
|
|
159
176
|
content: msg.content
|
|
160
177
|
})),
|
|
161
|
-
|
|
178
|
+
model: this.model,
|
|
179
|
+
...tools ? {
|
|
180
|
+
tools: tools.map((tool) => ({
|
|
181
|
+
type: "function",
|
|
182
|
+
function: {
|
|
183
|
+
name: tool.function.name,
|
|
184
|
+
description: tool.function.description,
|
|
185
|
+
parameters: tool.function.parameters
|
|
186
|
+
}
|
|
187
|
+
})),
|
|
188
|
+
tool_choice: "auto"
|
|
189
|
+
} : responseFormat ? {
|
|
190
|
+
response_format: {
|
|
191
|
+
type: responseFormat.type
|
|
192
|
+
}
|
|
193
|
+
} : {}
|
|
162
194
|
});
|
|
163
|
-
|
|
195
|
+
const response = completion.choices[0].message;
|
|
196
|
+
if (response.tool_calls) {
|
|
197
|
+
return {
|
|
198
|
+
content: response.content || "",
|
|
199
|
+
role: response.role,
|
|
200
|
+
toolCalls: response.tool_calls.map((call) => ({
|
|
201
|
+
name: call.function.name,
|
|
202
|
+
arguments: call.function.arguments
|
|
203
|
+
}))
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return response.content || "";
|
|
164
207
|
}
|
|
165
208
|
async generateChat(messages) {
|
|
166
|
-
const
|
|
167
|
-
model: this.model,
|
|
209
|
+
const completion = await this.openai.chat.completions.create({
|
|
168
210
|
messages: messages.map((msg) => ({
|
|
169
211
|
role: msg.role,
|
|
170
212
|
content: msg.content
|
|
171
|
-
}))
|
|
213
|
+
})),
|
|
214
|
+
model: this.model
|
|
172
215
|
});
|
|
173
|
-
const
|
|
216
|
+
const response = completion.choices[0].message;
|
|
174
217
|
return {
|
|
175
|
-
content:
|
|
176
|
-
role:
|
|
218
|
+
content: response.content || "",
|
|
219
|
+
role: response.role
|
|
177
220
|
};
|
|
178
221
|
}
|
|
179
222
|
};
|
|
@@ -1014,7 +1057,7 @@ var EmbedderFactory = class {
|
|
|
1014
1057
|
};
|
|
1015
1058
|
var LLMFactory = class {
|
|
1016
1059
|
static create(provider, config) {
|
|
1017
|
-
switch (provider
|
|
1060
|
+
switch (provider) {
|
|
1018
1061
|
case "openai":
|
|
1019
1062
|
return new OpenAILLM(config);
|
|
1020
1063
|
case "openai_structured":
|
|
@@ -1080,7 +1123,7 @@ function getFactRetrievalMessages(parsedMessages) {
|
|
|
1080
1123
|
Input: Me favourite movies are Inception and Interstellar.
|
|
1081
1124
|
Output: {"facts" : ["Favourite movies are Inception and Interstellar"]}
|
|
1082
1125
|
|
|
1083
|
-
Return the facts and preferences in a
|
|
1126
|
+
Return the facts and preferences in a JSON format as shown above. You MUST return a valid JSON object with a 'facts' key containing an array of strings.
|
|
1084
1127
|
|
|
1085
1128
|
Remember the following:
|
|
1086
1129
|
- Today's date is ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.
|
|
@@ -1089,16 +1132,16 @@ function getFactRetrievalMessages(parsedMessages) {
|
|
|
1089
1132
|
- If the user asks where you fetched my information, answer that you found from publicly available sources on internet.
|
|
1090
1133
|
- If you do not find anything relevant in the below conversation, you can return an empty list corresponding to the "facts" key.
|
|
1091
1134
|
- Create the facts based on the user and assistant messages only. Do not pick anything from the system messages.
|
|
1092
|
-
- Make sure to return the response in the format mentioned in the examples. The response should be in
|
|
1135
|
+
- Make sure to return the response in the JSON format mentioned in the examples. The response should be in JSON with a key as "facts" and corresponding value will be a list of strings.
|
|
1093
1136
|
- DO NOT RETURN ANYTHING ELSE OTHER THAN THE JSON FORMAT.
|
|
1094
|
-
- DO NOT ADD ANY ADDITIONAL TEXT OR CODEBLOCK IN THE JSON FIELDS WHICH MAKE IT
|
|
1137
|
+
- DO NOT ADD ANY ADDITIONAL TEXT OR CODEBLOCK IN THE JSON FIELDS WHICH MAKE IT INVALID SUCH AS "\`\`\`json" OR "\`\`\`".
|
|
1095
1138
|
- You should detect the language of the user input and record the facts in the same language.
|
|
1096
1139
|
- For basic factual statements, break them down into individual facts if they contain multiple pieces of information.
|
|
1097
1140
|
|
|
1098
|
-
Following is a conversation between the user and the assistant. You have to extract the relevant facts and preferences about the user, if any, from the conversation and return them in the
|
|
1141
|
+
Following is a conversation between the user and the assistant. You have to extract the relevant facts and preferences about the user, if any, from the conversation and return them in the JSON format as shown above.
|
|
1099
1142
|
You should detect the language of the user input and record the facts in the same language.
|
|
1100
1143
|
`;
|
|
1101
|
-
const userPrompt = `Following is a conversation between the user and the assistant. You have to extract the relevant facts and preferences about the user, if any, from the conversation and return them in the
|
|
1144
|
+
const userPrompt = `Following is a conversation between the user and the assistant. You have to extract the relevant facts and preferences about the user, if any, from the conversation and return them in the JSON format as shown above.
|
|
1102
1145
|
|
|
1103
1146
|
Input:
|
|
1104
1147
|
${parsedMessages}`;
|
|
@@ -1261,14 +1304,14 @@ function getUpdateMemoryMessages(retrievedOldMemory, newRetrievedFacts) {
|
|
|
1261
1304
|
${JSON.stringify(newRetrievedFacts, null, 2)}
|
|
1262
1305
|
|
|
1263
1306
|
Follow the instruction mentioned below:
|
|
1264
|
-
- Do not return anything from the custom few shot prompts provided above.
|
|
1307
|
+
- Do not return anything from the custom few shot example prompts provided above.
|
|
1265
1308
|
- If the current memory is empty, then you have to add the new retrieved facts to the memory.
|
|
1266
1309
|
- You should return the updated memory in only JSON format as shown below. The memory key should be the same if no changes are made.
|
|
1267
1310
|
- If there is an addition, generate a new key and add the new memory corresponding to it.
|
|
1268
1311
|
- If there is a deletion, the memory key-value pair should be removed from the memory.
|
|
1269
1312
|
- If there is an update, the ID key should remain the same and only the value needs to be updated.
|
|
1270
1313
|
- DO NOT RETURN ANYTHING ELSE OTHER THAN THE JSON FORMAT.
|
|
1271
|
-
- DO NOT ADD ANY ADDITIONAL TEXT OR CODEBLOCK IN THE JSON FIELDS WHICH MAKE IT
|
|
1314
|
+
- DO NOT ADD ANY ADDITIONAL TEXT OR CODEBLOCK IN THE JSON FIELDS WHICH MAKE IT INVALID SUCH AS "\`\`\`json" OR "\`\`\`".
|
|
1272
1315
|
|
|
1273
1316
|
Do not return anything except the JSON format.`;
|
|
1274
1317
|
}
|
|
@@ -1368,6 +1411,21 @@ var DEFAULT_MEMORY_CONFIG = {
|
|
|
1368
1411
|
model: "gpt-4-turbo-preview"
|
|
1369
1412
|
}
|
|
1370
1413
|
},
|
|
1414
|
+
enableGraph: false,
|
|
1415
|
+
graphStore: {
|
|
1416
|
+
provider: "neo4j",
|
|
1417
|
+
config: {
|
|
1418
|
+
url: process.env.NEO4J_URL || "neo4j://localhost:7687",
|
|
1419
|
+
username: process.env.NEO4J_USERNAME || "neo4j",
|
|
1420
|
+
password: process.env.NEO4J_PASSWORD || "password"
|
|
1421
|
+
},
|
|
1422
|
+
llm: {
|
|
1423
|
+
provider: "openai",
|
|
1424
|
+
config: {
|
|
1425
|
+
model: "gpt-4-turbo-preview"
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
},
|
|
1371
1429
|
historyDbPath: "memory.db"
|
|
1372
1430
|
};
|
|
1373
1431
|
|
|
@@ -1401,12 +1459,742 @@ var ConfigManager = class {
|
|
|
1401
1459
|
},
|
|
1402
1460
|
historyDbPath: userConfig.historyDbPath || DEFAULT_MEMORY_CONFIG.historyDbPath,
|
|
1403
1461
|
customPrompt: userConfig.customPrompt,
|
|
1404
|
-
graphStore:
|
|
1462
|
+
graphStore: {
|
|
1463
|
+
...DEFAULT_MEMORY_CONFIG.graphStore,
|
|
1464
|
+
...userConfig.graphStore
|
|
1465
|
+
},
|
|
1466
|
+
enableGraph: userConfig.enableGraph || DEFAULT_MEMORY_CONFIG.enableGraph
|
|
1405
1467
|
};
|
|
1406
1468
|
return MemoryConfigSchema.parse(mergedConfig);
|
|
1407
1469
|
}
|
|
1408
1470
|
};
|
|
1409
1471
|
|
|
1472
|
+
// src/oss/src/memory/graph_memory.ts
|
|
1473
|
+
var import_neo4j_driver = __toESM(require("neo4j-driver"));
|
|
1474
|
+
|
|
1475
|
+
// src/oss/src/utils/bm25.ts
|
|
1476
|
+
var BM25 = class {
|
|
1477
|
+
constructor(documents, k1 = 1.5, b = 0.75) {
|
|
1478
|
+
this.documents = documents;
|
|
1479
|
+
this.k1 = k1;
|
|
1480
|
+
this.b = b;
|
|
1481
|
+
this.docLengths = documents.map((doc) => doc.length);
|
|
1482
|
+
this.avgDocLength = this.docLengths.reduce((a, b2) => a + b2, 0) / documents.length;
|
|
1483
|
+
this.docFreq = /* @__PURE__ */ new Map();
|
|
1484
|
+
this.idf = /* @__PURE__ */ new Map();
|
|
1485
|
+
this.computeIdf();
|
|
1486
|
+
}
|
|
1487
|
+
computeIdf() {
|
|
1488
|
+
const N = this.documents.length;
|
|
1489
|
+
for (const doc of this.documents) {
|
|
1490
|
+
const terms = new Set(doc);
|
|
1491
|
+
for (const term of terms) {
|
|
1492
|
+
this.docFreq.set(term, (this.docFreq.get(term) || 0) + 1);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
for (const [term, freq] of this.docFreq) {
|
|
1496
|
+
this.idf.set(term, Math.log((N - freq + 0.5) / (freq + 0.5) + 1));
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
score(query, doc, index) {
|
|
1500
|
+
let score = 0;
|
|
1501
|
+
const docLength = this.docLengths[index];
|
|
1502
|
+
for (const term of query) {
|
|
1503
|
+
const tf = doc.filter((t) => t === term).length;
|
|
1504
|
+
const idf = this.idf.get(term) || 0;
|
|
1505
|
+
score += idf * tf * (this.k1 + 1) / (tf + this.k1 * (1 - this.b + this.b * docLength / this.avgDocLength));
|
|
1506
|
+
}
|
|
1507
|
+
return score;
|
|
1508
|
+
}
|
|
1509
|
+
search(query) {
|
|
1510
|
+
const scores = this.documents.map((doc, idx) => ({
|
|
1511
|
+
doc,
|
|
1512
|
+
score: this.score(query, doc, idx)
|
|
1513
|
+
}));
|
|
1514
|
+
return scores.sort((a, b) => b.score - a.score).map((item) => item.doc);
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
|
|
1518
|
+
// src/oss/src/graphs/tools.ts
|
|
1519
|
+
var RELATIONS_TOOL = {
|
|
1520
|
+
type: "function",
|
|
1521
|
+
function: {
|
|
1522
|
+
name: "establish_relationships",
|
|
1523
|
+
description: "Establish relationships among the entities based on the provided text.",
|
|
1524
|
+
parameters: {
|
|
1525
|
+
type: "object",
|
|
1526
|
+
properties: {
|
|
1527
|
+
entities: {
|
|
1528
|
+
type: "array",
|
|
1529
|
+
items: {
|
|
1530
|
+
type: "object",
|
|
1531
|
+
properties: {
|
|
1532
|
+
source: {
|
|
1533
|
+
type: "string",
|
|
1534
|
+
description: "The source entity of the relationship."
|
|
1535
|
+
},
|
|
1536
|
+
relationship: {
|
|
1537
|
+
type: "string",
|
|
1538
|
+
description: "The relationship between the source and destination entities."
|
|
1539
|
+
},
|
|
1540
|
+
destination: {
|
|
1541
|
+
type: "string",
|
|
1542
|
+
description: "The destination entity of the relationship."
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1545
|
+
required: ["source", "relationship", "destination"],
|
|
1546
|
+
additionalProperties: false
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
},
|
|
1550
|
+
required: ["entities"],
|
|
1551
|
+
additionalProperties: false
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
var EXTRACT_ENTITIES_TOOL = {
|
|
1556
|
+
type: "function",
|
|
1557
|
+
function: {
|
|
1558
|
+
name: "extract_entities",
|
|
1559
|
+
description: "Extract entities and their types from the text.",
|
|
1560
|
+
parameters: {
|
|
1561
|
+
type: "object",
|
|
1562
|
+
properties: {
|
|
1563
|
+
entities: {
|
|
1564
|
+
type: "array",
|
|
1565
|
+
items: {
|
|
1566
|
+
type: "object",
|
|
1567
|
+
properties: {
|
|
1568
|
+
entity: {
|
|
1569
|
+
type: "string",
|
|
1570
|
+
description: "The name or identifier of the entity."
|
|
1571
|
+
},
|
|
1572
|
+
entity_type: {
|
|
1573
|
+
type: "string",
|
|
1574
|
+
description: "The type or category of the entity."
|
|
1575
|
+
}
|
|
1576
|
+
},
|
|
1577
|
+
required: ["entity", "entity_type"],
|
|
1578
|
+
additionalProperties: false
|
|
1579
|
+
},
|
|
1580
|
+
description: "An array of entities with their types."
|
|
1581
|
+
}
|
|
1582
|
+
},
|
|
1583
|
+
required: ["entities"],
|
|
1584
|
+
additionalProperties: false
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
var DELETE_MEMORY_TOOL_GRAPH = {
|
|
1589
|
+
type: "function",
|
|
1590
|
+
function: {
|
|
1591
|
+
name: "delete_graph_memory",
|
|
1592
|
+
description: "Delete the relationship between two nodes.",
|
|
1593
|
+
parameters: {
|
|
1594
|
+
type: "object",
|
|
1595
|
+
properties: {
|
|
1596
|
+
source: {
|
|
1597
|
+
type: "string",
|
|
1598
|
+
description: "The identifier of the source node in the relationship."
|
|
1599
|
+
},
|
|
1600
|
+
relationship: {
|
|
1601
|
+
type: "string",
|
|
1602
|
+
description: "The existing relationship between the source and destination nodes that needs to be deleted."
|
|
1603
|
+
},
|
|
1604
|
+
destination: {
|
|
1605
|
+
type: "string",
|
|
1606
|
+
description: "The identifier of the destination node in the relationship."
|
|
1607
|
+
}
|
|
1608
|
+
},
|
|
1609
|
+
required: ["source", "relationship", "destination"],
|
|
1610
|
+
additionalProperties: false
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
};
|
|
1614
|
+
|
|
1615
|
+
// src/oss/src/graphs/utils.ts
|
|
1616
|
+
var EXTRACT_RELATIONS_PROMPT = `
|
|
1617
|
+
You are an advanced algorithm designed to extract structured information from text to construct knowledge graphs. Your goal is to capture comprehensive and accurate information. Follow these key principles:
|
|
1618
|
+
|
|
1619
|
+
1. Extract only explicitly stated information from the text.
|
|
1620
|
+
2. Establish relationships among the entities provided.
|
|
1621
|
+
3. Use "USER_ID" as the source entity for any self-references (e.g., "I," "me," "my," etc.) in user messages.
|
|
1622
|
+
CUSTOM_PROMPT
|
|
1623
|
+
|
|
1624
|
+
Relationships:
|
|
1625
|
+
- Use consistent, general, and timeless relationship types.
|
|
1626
|
+
- Example: Prefer "professor" over "became_professor."
|
|
1627
|
+
- Relationships should only be established among the entities explicitly mentioned in the user message.
|
|
1628
|
+
|
|
1629
|
+
Entity Consistency:
|
|
1630
|
+
- Ensure that relationships are coherent and logically align with the context of the message.
|
|
1631
|
+
- Maintain consistent naming for entities across the extracted data.
|
|
1632
|
+
|
|
1633
|
+
Strive to construct a coherent and easily understandable knowledge graph by eshtablishing all the relationships among the entities and adherence to the user's context.
|
|
1634
|
+
|
|
1635
|
+
Adhere strictly to these guidelines to ensure high-quality knowledge graph extraction.
|
|
1636
|
+
`;
|
|
1637
|
+
var DELETE_RELATIONS_SYSTEM_PROMPT = `
|
|
1638
|
+
You are a graph memory manager specializing in identifying, managing, and optimizing relationships within graph-based memories. Your primary task is to analyze a list of existing relationships and determine which ones should be deleted based on the new information provided.
|
|
1639
|
+
Input:
|
|
1640
|
+
1. Existing Graph Memories: A list of current graph memories, each containing source, relationship, and destination information.
|
|
1641
|
+
2. New Text: The new information to be integrated into the existing graph structure.
|
|
1642
|
+
3. Use "USER_ID" as node for any self-references (e.g., "I," "me," "my," etc.) in user messages.
|
|
1643
|
+
|
|
1644
|
+
Guidelines:
|
|
1645
|
+
1. Identification: Use the new information to evaluate existing relationships in the memory graph.
|
|
1646
|
+
2. Deletion Criteria: Delete a relationship only if it meets at least one of these conditions:
|
|
1647
|
+
- Outdated or Inaccurate: The new information is more recent or accurate.
|
|
1648
|
+
- Contradictory: The new information conflicts with or negates the existing information.
|
|
1649
|
+
3. DO NOT DELETE if their is a possibility of same type of relationship but different destination nodes.
|
|
1650
|
+
4. Comprehensive Analysis:
|
|
1651
|
+
- Thoroughly examine each existing relationship against the new information and delete as necessary.
|
|
1652
|
+
- Multiple deletions may be required based on the new information.
|
|
1653
|
+
5. Semantic Integrity:
|
|
1654
|
+
- Ensure that deletions maintain or improve the overall semantic structure of the graph.
|
|
1655
|
+
- Avoid deleting relationships that are NOT contradictory/outdated to the new information.
|
|
1656
|
+
6. Temporal Awareness: Prioritize recency when timestamps are available.
|
|
1657
|
+
7. Necessity Principle: Only DELETE relationships that must be deleted and are contradictory/outdated to the new information to maintain an accurate and coherent memory graph.
|
|
1658
|
+
|
|
1659
|
+
Note: DO NOT DELETE if their is a possibility of same type of relationship but different destination nodes.
|
|
1660
|
+
|
|
1661
|
+
For example:
|
|
1662
|
+
Existing Memory: alice -- loves_to_eat -- pizza
|
|
1663
|
+
New Information: Alice also loves to eat burger.
|
|
1664
|
+
|
|
1665
|
+
Do not delete in the above example because there is a possibility that Alice loves to eat both pizza and burger.
|
|
1666
|
+
|
|
1667
|
+
Memory Format:
|
|
1668
|
+
source -- relationship -- destination
|
|
1669
|
+
|
|
1670
|
+
Provide a list of deletion instructions, each specifying the relationship to be deleted.
|
|
1671
|
+
`;
|
|
1672
|
+
function getDeleteMessages(existingMemoriesString, data, userId) {
|
|
1673
|
+
return [
|
|
1674
|
+
DELETE_RELATIONS_SYSTEM_PROMPT.replace("USER_ID", userId),
|
|
1675
|
+
`Here are the existing memories: ${existingMemoriesString}
|
|
1676
|
+
|
|
1677
|
+
New Information: ${data}`
|
|
1678
|
+
];
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// src/oss/src/utils/logger.ts
|
|
1682
|
+
var logger = {
|
|
1683
|
+
info: (message) => console.log(`[INFO] ${message}`),
|
|
1684
|
+
error: (message) => console.error(`[ERROR] ${message}`),
|
|
1685
|
+
debug: (message) => console.debug(`[DEBUG] ${message}`),
|
|
1686
|
+
warn: (message) => console.warn(`[WARN] ${message}`)
|
|
1687
|
+
};
|
|
1688
|
+
|
|
1689
|
+
// src/oss/src/memory/graph_memory.ts
|
|
1690
|
+
var MemoryGraph = class {
|
|
1691
|
+
constructor(config) {
|
|
1692
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
1693
|
+
this.config = config;
|
|
1694
|
+
if (!((_b = (_a = config.graphStore) == null ? void 0 : _a.config) == null ? void 0 : _b.url) || !((_d = (_c = config.graphStore) == null ? void 0 : _c.config) == null ? void 0 : _d.username) || !((_f = (_e = config.graphStore) == null ? void 0 : _e.config) == null ? void 0 : _f.password)) {
|
|
1695
|
+
throw new Error("Neo4j configuration is incomplete");
|
|
1696
|
+
}
|
|
1697
|
+
this.graph = import_neo4j_driver.default.driver(
|
|
1698
|
+
config.graphStore.config.url,
|
|
1699
|
+
import_neo4j_driver.default.auth.basic(
|
|
1700
|
+
config.graphStore.config.username,
|
|
1701
|
+
config.graphStore.config.password
|
|
1702
|
+
)
|
|
1703
|
+
);
|
|
1704
|
+
this.embeddingModel = EmbedderFactory.create(
|
|
1705
|
+
this.config.embedder.provider,
|
|
1706
|
+
this.config.embedder.config
|
|
1707
|
+
);
|
|
1708
|
+
this.llmProvider = "openai";
|
|
1709
|
+
if ((_g = this.config.llm) == null ? void 0 : _g.provider) {
|
|
1710
|
+
this.llmProvider = this.config.llm.provider;
|
|
1711
|
+
}
|
|
1712
|
+
if ((_i = (_h = this.config.graphStore) == null ? void 0 : _h.llm) == null ? void 0 : _i.provider) {
|
|
1713
|
+
this.llmProvider = this.config.graphStore.llm.provider;
|
|
1714
|
+
}
|
|
1715
|
+
this.llm = LLMFactory.create(this.llmProvider, this.config.llm.config);
|
|
1716
|
+
this.structuredLlm = LLMFactory.create(
|
|
1717
|
+
"openai_structured",
|
|
1718
|
+
this.config.llm.config
|
|
1719
|
+
);
|
|
1720
|
+
this.threshold = 0.7;
|
|
1721
|
+
}
|
|
1722
|
+
async add(data, filters) {
|
|
1723
|
+
const entityTypeMap = await this._retrieveNodesFromData(data, filters);
|
|
1724
|
+
const toBeAdded = await this._establishNodesRelationsFromData(
|
|
1725
|
+
data,
|
|
1726
|
+
filters,
|
|
1727
|
+
entityTypeMap
|
|
1728
|
+
);
|
|
1729
|
+
const searchOutput = await this._searchGraphDb(
|
|
1730
|
+
Object.keys(entityTypeMap),
|
|
1731
|
+
filters
|
|
1732
|
+
);
|
|
1733
|
+
const toBeDeleted = await this._getDeleteEntitiesFromSearchOutput(
|
|
1734
|
+
searchOutput,
|
|
1735
|
+
data,
|
|
1736
|
+
filters
|
|
1737
|
+
);
|
|
1738
|
+
const deletedEntities = await this._deleteEntities(
|
|
1739
|
+
toBeDeleted,
|
|
1740
|
+
filters["userId"]
|
|
1741
|
+
);
|
|
1742
|
+
const addedEntities = await this._addEntities(
|
|
1743
|
+
toBeAdded,
|
|
1744
|
+
filters["userId"],
|
|
1745
|
+
entityTypeMap
|
|
1746
|
+
);
|
|
1747
|
+
return {
|
|
1748
|
+
deleted_entities: deletedEntities,
|
|
1749
|
+
added_entities: addedEntities,
|
|
1750
|
+
relations: toBeAdded
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
async search(query, filters, limit = 100) {
|
|
1754
|
+
const entityTypeMap = await this._retrieveNodesFromData(query, filters);
|
|
1755
|
+
const searchOutput = await this._searchGraphDb(
|
|
1756
|
+
Object.keys(entityTypeMap),
|
|
1757
|
+
filters
|
|
1758
|
+
);
|
|
1759
|
+
if (!searchOutput.length) {
|
|
1760
|
+
return [];
|
|
1761
|
+
}
|
|
1762
|
+
const searchOutputsSequence = searchOutput.map((item) => [
|
|
1763
|
+
item.source,
|
|
1764
|
+
item.relationship,
|
|
1765
|
+
item.destination
|
|
1766
|
+
]);
|
|
1767
|
+
const bm25 = new BM25(searchOutputsSequence);
|
|
1768
|
+
const tokenizedQuery = query.split(" ");
|
|
1769
|
+
const rerankedResults = bm25.search(tokenizedQuery).slice(0, 5);
|
|
1770
|
+
const searchResults = rerankedResults.map((item) => ({
|
|
1771
|
+
source: item[0],
|
|
1772
|
+
relationship: item[1],
|
|
1773
|
+
destination: item[2]
|
|
1774
|
+
}));
|
|
1775
|
+
logger.info(`Returned ${searchResults.length} search results`);
|
|
1776
|
+
return searchResults;
|
|
1777
|
+
}
|
|
1778
|
+
async deleteAll(filters) {
|
|
1779
|
+
const session = this.graph.session();
|
|
1780
|
+
try {
|
|
1781
|
+
await session.run("MATCH (n {user_id: $user_id}) DETACH DELETE n", {
|
|
1782
|
+
user_id: filters["userId"]
|
|
1783
|
+
});
|
|
1784
|
+
} finally {
|
|
1785
|
+
await session.close();
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
async getAll(filters, limit = 100) {
|
|
1789
|
+
const session = this.graph.session();
|
|
1790
|
+
try {
|
|
1791
|
+
const result = await session.run(
|
|
1792
|
+
`
|
|
1793
|
+
MATCH (n {user_id: $user_id})-[r]->(m {user_id: $user_id})
|
|
1794
|
+
RETURN n.name AS source, type(r) AS relationship, m.name AS target
|
|
1795
|
+
LIMIT toInteger($limit)
|
|
1796
|
+
`,
|
|
1797
|
+
{ user_id: filters["userId"], limit: Math.floor(Number(limit)) }
|
|
1798
|
+
);
|
|
1799
|
+
const finalResults = result.records.map((record) => ({
|
|
1800
|
+
source: record.get("source"),
|
|
1801
|
+
relationship: record.get("relationship"),
|
|
1802
|
+
target: record.get("target")
|
|
1803
|
+
}));
|
|
1804
|
+
logger.info(`Retrieved ${finalResults.length} relationships`);
|
|
1805
|
+
return finalResults;
|
|
1806
|
+
} finally {
|
|
1807
|
+
await session.close();
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
async _retrieveNodesFromData(data, filters) {
|
|
1811
|
+
const tools = [EXTRACT_ENTITIES_TOOL];
|
|
1812
|
+
const searchResults = await this.structuredLlm.generateResponse(
|
|
1813
|
+
[
|
|
1814
|
+
{
|
|
1815
|
+
role: "system",
|
|
1816
|
+
content: `You are a smart assistant who understands entities and their types in a given text. If user message contains self reference such as 'I', 'me', 'my' etc. then use ${filters["userId"]} as the source entity. Extract all the entities from the text. ***DO NOT*** answer the question itself if the given text is a question.`
|
|
1817
|
+
},
|
|
1818
|
+
{ role: "user", content: data }
|
|
1819
|
+
],
|
|
1820
|
+
{ type: "json_object" },
|
|
1821
|
+
tools
|
|
1822
|
+
);
|
|
1823
|
+
let entityTypeMap = {};
|
|
1824
|
+
try {
|
|
1825
|
+
if (typeof searchResults !== "string" && searchResults.toolCalls) {
|
|
1826
|
+
for (const call of searchResults.toolCalls) {
|
|
1827
|
+
if (call.name === "extract_entities") {
|
|
1828
|
+
const args = JSON.parse(call.arguments);
|
|
1829
|
+
for (const item of args.entities) {
|
|
1830
|
+
entityTypeMap[item.entity] = item.entity_type;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
} catch (e) {
|
|
1836
|
+
logger.error(`Error in search tool: ${e}`);
|
|
1837
|
+
}
|
|
1838
|
+
entityTypeMap = Object.fromEntries(
|
|
1839
|
+
Object.entries(entityTypeMap).map(([k, v]) => [
|
|
1840
|
+
k.toLowerCase().replace(/ /g, "_"),
|
|
1841
|
+
v.toLowerCase().replace(/ /g, "_")
|
|
1842
|
+
])
|
|
1843
|
+
);
|
|
1844
|
+
logger.debug(`Entity type map: ${JSON.stringify(entityTypeMap)}`);
|
|
1845
|
+
return entityTypeMap;
|
|
1846
|
+
}
|
|
1847
|
+
async _establishNodesRelationsFromData(data, filters, entityTypeMap) {
|
|
1848
|
+
var _a;
|
|
1849
|
+
let messages;
|
|
1850
|
+
if ((_a = this.config.graphStore) == null ? void 0 : _a.customPrompt) {
|
|
1851
|
+
messages = [
|
|
1852
|
+
{
|
|
1853
|
+
role: "system",
|
|
1854
|
+
content: EXTRACT_RELATIONS_PROMPT.replace(
|
|
1855
|
+
"USER_ID",
|
|
1856
|
+
filters["userId"]
|
|
1857
|
+
).replace(
|
|
1858
|
+
"CUSTOM_PROMPT",
|
|
1859
|
+
`4. ${this.config.graphStore.customPrompt}`
|
|
1860
|
+
) + "\nPlease provide your response in JSON format."
|
|
1861
|
+
},
|
|
1862
|
+
{ role: "user", content: data }
|
|
1863
|
+
];
|
|
1864
|
+
} else {
|
|
1865
|
+
messages = [
|
|
1866
|
+
{
|
|
1867
|
+
role: "system",
|
|
1868
|
+
content: EXTRACT_RELATIONS_PROMPT.replace("USER_ID", filters["userId"]) + "\nPlease provide your response in JSON format."
|
|
1869
|
+
},
|
|
1870
|
+
{
|
|
1871
|
+
role: "user",
|
|
1872
|
+
content: `List of entities: ${Object.keys(entityTypeMap)}.
|
|
1873
|
+
|
|
1874
|
+
Text: ${data}`
|
|
1875
|
+
}
|
|
1876
|
+
];
|
|
1877
|
+
}
|
|
1878
|
+
const tools = [RELATIONS_TOOL];
|
|
1879
|
+
const extractedEntities = await this.structuredLlm.generateResponse(
|
|
1880
|
+
messages,
|
|
1881
|
+
{ type: "json_object" },
|
|
1882
|
+
tools
|
|
1883
|
+
);
|
|
1884
|
+
let entities = [];
|
|
1885
|
+
if (typeof extractedEntities !== "string" && extractedEntities.toolCalls) {
|
|
1886
|
+
const toolCall = extractedEntities.toolCalls[0];
|
|
1887
|
+
if (toolCall && toolCall.arguments) {
|
|
1888
|
+
const args = JSON.parse(toolCall.arguments);
|
|
1889
|
+
entities = args.entities || [];
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
entities = this._removeSpacesFromEntities(entities);
|
|
1893
|
+
logger.debug(`Extracted entities: ${JSON.stringify(entities)}`);
|
|
1894
|
+
return entities;
|
|
1895
|
+
}
|
|
1896
|
+
async _searchGraphDb(nodeList, filters, limit = 100) {
|
|
1897
|
+
const resultRelations = [];
|
|
1898
|
+
const session = this.graph.session();
|
|
1899
|
+
try {
|
|
1900
|
+
for (const node of nodeList) {
|
|
1901
|
+
const nEmbedding = await this.embeddingModel.embed(node);
|
|
1902
|
+
const cypher = `
|
|
1903
|
+
MATCH (n)
|
|
1904
|
+
WHERE n.embedding IS NOT NULL AND n.user_id = $user_id
|
|
1905
|
+
WITH n,
|
|
1906
|
+
round(reduce(dot = 0.0, i IN range(0, size(n.embedding)-1) | dot + n.embedding[i] * $n_embedding[i]) /
|
|
1907
|
+
(sqrt(reduce(l2 = 0.0, i IN range(0, size(n.embedding)-1) | l2 + n.embedding[i] * n.embedding[i])) *
|
|
1908
|
+
sqrt(reduce(l2 = 0.0, i IN range(0, size($n_embedding)-1) | l2 + $n_embedding[i] * $n_embedding[i]))), 4) AS similarity
|
|
1909
|
+
WHERE similarity >= $threshold
|
|
1910
|
+
MATCH (n)-[r]->(m)
|
|
1911
|
+
RETURN n.name AS source, elementId(n) AS source_id, type(r) AS relationship, elementId(r) AS relation_id, m.name AS destination, elementId(m) AS destination_id, similarity
|
|
1912
|
+
UNION
|
|
1913
|
+
MATCH (n)
|
|
1914
|
+
WHERE n.embedding IS NOT NULL AND n.user_id = $user_id
|
|
1915
|
+
WITH n,
|
|
1916
|
+
round(reduce(dot = 0.0, i IN range(0, size(n.embedding)-1) | dot + n.embedding[i] * $n_embedding[i]) /
|
|
1917
|
+
(sqrt(reduce(l2 = 0.0, i IN range(0, size(n.embedding)-1) | l2 + n.embedding[i] * n.embedding[i])) *
|
|
1918
|
+
sqrt(reduce(l2 = 0.0, i IN range(0, size($n_embedding)-1) | l2 + $n_embedding[i] * $n_embedding[i]))), 4) AS similarity
|
|
1919
|
+
WHERE similarity >= $threshold
|
|
1920
|
+
MATCH (m)-[r]->(n)
|
|
1921
|
+
RETURN m.name AS source, elementId(m) AS source_id, type(r) AS relationship, elementId(r) AS relation_id, n.name AS destination, elementId(n) AS destination_id, similarity
|
|
1922
|
+
ORDER BY similarity DESC
|
|
1923
|
+
LIMIT toInteger($limit)
|
|
1924
|
+
`;
|
|
1925
|
+
const result = await session.run(cypher, {
|
|
1926
|
+
n_embedding: nEmbedding,
|
|
1927
|
+
threshold: this.threshold,
|
|
1928
|
+
user_id: filters["userId"],
|
|
1929
|
+
limit: Math.floor(Number(limit))
|
|
1930
|
+
});
|
|
1931
|
+
resultRelations.push(
|
|
1932
|
+
...result.records.map((record) => ({
|
|
1933
|
+
source: record.get("source"),
|
|
1934
|
+
source_id: record.get("source_id").toString(),
|
|
1935
|
+
relationship: record.get("relationship"),
|
|
1936
|
+
relation_id: record.get("relation_id").toString(),
|
|
1937
|
+
destination: record.get("destination"),
|
|
1938
|
+
destination_id: record.get("destination_id").toString(),
|
|
1939
|
+
similarity: record.get("similarity")
|
|
1940
|
+
}))
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
} finally {
|
|
1944
|
+
await session.close();
|
|
1945
|
+
}
|
|
1946
|
+
return resultRelations;
|
|
1947
|
+
}
|
|
1948
|
+
async _getDeleteEntitiesFromSearchOutput(searchOutput, data, filters) {
|
|
1949
|
+
const searchOutputString = searchOutput.map(
|
|
1950
|
+
(item) => `${item.source} -- ${item.relationship} -- ${item.destination}`
|
|
1951
|
+
).join("\n");
|
|
1952
|
+
const [systemPrompt, userPrompt] = getDeleteMessages(
|
|
1953
|
+
searchOutputString,
|
|
1954
|
+
data,
|
|
1955
|
+
filters["userId"]
|
|
1956
|
+
);
|
|
1957
|
+
const tools = [DELETE_MEMORY_TOOL_GRAPH];
|
|
1958
|
+
const memoryUpdates = await this.structuredLlm.generateResponse(
|
|
1959
|
+
[
|
|
1960
|
+
{ role: "system", content: systemPrompt },
|
|
1961
|
+
{ role: "user", content: userPrompt }
|
|
1962
|
+
],
|
|
1963
|
+
{ type: "json_object" },
|
|
1964
|
+
tools
|
|
1965
|
+
);
|
|
1966
|
+
const toBeDeleted = [];
|
|
1967
|
+
if (typeof memoryUpdates !== "string" && memoryUpdates.toolCalls) {
|
|
1968
|
+
for (const item of memoryUpdates.toolCalls) {
|
|
1969
|
+
if (item.name === "delete_graph_memory") {
|
|
1970
|
+
toBeDeleted.push(JSON.parse(item.arguments));
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
const cleanedToBeDeleted = this._removeSpacesFromEntities(toBeDeleted);
|
|
1975
|
+
logger.debug(
|
|
1976
|
+
`Deleted relationships: ${JSON.stringify(cleanedToBeDeleted)}`
|
|
1977
|
+
);
|
|
1978
|
+
return cleanedToBeDeleted;
|
|
1979
|
+
}
|
|
1980
|
+
async _deleteEntities(toBeDeleted, userId) {
|
|
1981
|
+
const results = [];
|
|
1982
|
+
const session = this.graph.session();
|
|
1983
|
+
try {
|
|
1984
|
+
for (const item of toBeDeleted) {
|
|
1985
|
+
const { source, destination, relationship } = item;
|
|
1986
|
+
const cypher = `
|
|
1987
|
+
MATCH (n {name: $source_name, user_id: $user_id})
|
|
1988
|
+
-[r:${relationship}]->
|
|
1989
|
+
(m {name: $dest_name, user_id: $user_id})
|
|
1990
|
+
DELETE r
|
|
1991
|
+
RETURN
|
|
1992
|
+
n.name AS source,
|
|
1993
|
+
m.name AS target,
|
|
1994
|
+
type(r) AS relationship
|
|
1995
|
+
`;
|
|
1996
|
+
const result = await session.run(cypher, {
|
|
1997
|
+
source_name: source,
|
|
1998
|
+
dest_name: destination,
|
|
1999
|
+
user_id: userId
|
|
2000
|
+
});
|
|
2001
|
+
results.push(result.records);
|
|
2002
|
+
}
|
|
2003
|
+
} finally {
|
|
2004
|
+
await session.close();
|
|
2005
|
+
}
|
|
2006
|
+
return results;
|
|
2007
|
+
}
|
|
2008
|
+
async _addEntities(toBeAdded, userId, entityTypeMap) {
|
|
2009
|
+
var _a, _b;
|
|
2010
|
+
const results = [];
|
|
2011
|
+
const session = this.graph.session();
|
|
2012
|
+
try {
|
|
2013
|
+
for (const item of toBeAdded) {
|
|
2014
|
+
const { source, destination, relationship } = item;
|
|
2015
|
+
const sourceType = entityTypeMap[source] || "unknown";
|
|
2016
|
+
const destinationType = entityTypeMap[destination] || "unknown";
|
|
2017
|
+
const sourceEmbedding = await this.embeddingModel.embed(source);
|
|
2018
|
+
const destEmbedding = await this.embeddingModel.embed(destination);
|
|
2019
|
+
const sourceNodeSearchResult = await this._searchSourceNode(
|
|
2020
|
+
sourceEmbedding,
|
|
2021
|
+
userId
|
|
2022
|
+
);
|
|
2023
|
+
const destinationNodeSearchResult = await this._searchDestinationNode(
|
|
2024
|
+
destEmbedding,
|
|
2025
|
+
userId
|
|
2026
|
+
);
|
|
2027
|
+
let cypher;
|
|
2028
|
+
let params;
|
|
2029
|
+
if (destinationNodeSearchResult.length === 0 && sourceNodeSearchResult.length > 0) {
|
|
2030
|
+
cypher = `
|
|
2031
|
+
MATCH (source)
|
|
2032
|
+
WHERE elementId(source) = $source_id
|
|
2033
|
+
MERGE (destination:${destinationType} {name: $destination_name, user_id: $user_id})
|
|
2034
|
+
ON CREATE SET
|
|
2035
|
+
destination.created = timestamp(),
|
|
2036
|
+
destination.embedding = $destination_embedding
|
|
2037
|
+
MERGE (source)-[r:${relationship}]->(destination)
|
|
2038
|
+
ON CREATE SET
|
|
2039
|
+
r.created = timestamp()
|
|
2040
|
+
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
|
|
2041
|
+
`;
|
|
2042
|
+
params = {
|
|
2043
|
+
source_id: sourceNodeSearchResult[0].elementId,
|
|
2044
|
+
destination_name: destination,
|
|
2045
|
+
destination_embedding: destEmbedding,
|
|
2046
|
+
user_id: userId
|
|
2047
|
+
};
|
|
2048
|
+
} else if (destinationNodeSearchResult.length > 0 && sourceNodeSearchResult.length === 0) {
|
|
2049
|
+
cypher = `
|
|
2050
|
+
MATCH (destination)
|
|
2051
|
+
WHERE elementId(destination) = $destination_id
|
|
2052
|
+
MERGE (source:${sourceType} {name: $source_name, user_id: $user_id})
|
|
2053
|
+
ON CREATE SET
|
|
2054
|
+
source.created = timestamp(),
|
|
2055
|
+
source.embedding = $source_embedding
|
|
2056
|
+
MERGE (source)-[r:${relationship}]->(destination)
|
|
2057
|
+
ON CREATE SET
|
|
2058
|
+
r.created = timestamp()
|
|
2059
|
+
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
|
|
2060
|
+
`;
|
|
2061
|
+
params = {
|
|
2062
|
+
destination_id: destinationNodeSearchResult[0].elementId,
|
|
2063
|
+
source_name: source,
|
|
2064
|
+
source_embedding: sourceEmbedding,
|
|
2065
|
+
user_id: userId
|
|
2066
|
+
};
|
|
2067
|
+
} else if (sourceNodeSearchResult.length > 0 && destinationNodeSearchResult.length > 0) {
|
|
2068
|
+
cypher = `
|
|
2069
|
+
MATCH (source)
|
|
2070
|
+
WHERE elementId(source) = $source_id
|
|
2071
|
+
MATCH (destination)
|
|
2072
|
+
WHERE elementId(destination) = $destination_id
|
|
2073
|
+
MERGE (source)-[r:${relationship}]->(destination)
|
|
2074
|
+
ON CREATE SET
|
|
2075
|
+
r.created_at = timestamp(),
|
|
2076
|
+
r.updated_at = timestamp()
|
|
2077
|
+
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
|
|
2078
|
+
`;
|
|
2079
|
+
params = {
|
|
2080
|
+
source_id: (_a = sourceNodeSearchResult[0]) == null ? void 0 : _a.elementId,
|
|
2081
|
+
destination_id: (_b = destinationNodeSearchResult[0]) == null ? void 0 : _b.elementId,
|
|
2082
|
+
user_id: userId
|
|
2083
|
+
};
|
|
2084
|
+
} else {
|
|
2085
|
+
cypher = `
|
|
2086
|
+
MERGE (n:${sourceType} {name: $source_name, user_id: $user_id})
|
|
2087
|
+
ON CREATE SET n.created = timestamp(), n.embedding = $source_embedding
|
|
2088
|
+
ON MATCH SET n.embedding = $source_embedding
|
|
2089
|
+
MERGE (m:${destinationType} {name: $dest_name, user_id: $user_id})
|
|
2090
|
+
ON CREATE SET m.created = timestamp(), m.embedding = $dest_embedding
|
|
2091
|
+
ON MATCH SET m.embedding = $dest_embedding
|
|
2092
|
+
MERGE (n)-[rel:${relationship}]->(m)
|
|
2093
|
+
ON CREATE SET rel.created = timestamp()
|
|
2094
|
+
RETURN n.name AS source, type(rel) AS relationship, m.name AS target
|
|
2095
|
+
`;
|
|
2096
|
+
params = {
|
|
2097
|
+
source_name: source,
|
|
2098
|
+
dest_name: destination,
|
|
2099
|
+
source_embedding: sourceEmbedding,
|
|
2100
|
+
dest_embedding: destEmbedding,
|
|
2101
|
+
user_id: userId
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
const result = await session.run(cypher, params);
|
|
2105
|
+
results.push(result.records);
|
|
2106
|
+
}
|
|
2107
|
+
} finally {
|
|
2108
|
+
await session.close();
|
|
2109
|
+
}
|
|
2110
|
+
return results;
|
|
2111
|
+
}
|
|
2112
|
+
_removeSpacesFromEntities(entityList) {
|
|
2113
|
+
return entityList.map((item) => ({
|
|
2114
|
+
...item,
|
|
2115
|
+
source: item.source.toLowerCase().replace(/ /g, "_"),
|
|
2116
|
+
relationship: item.relationship.toLowerCase().replace(/ /g, "_"),
|
|
2117
|
+
destination: item.destination.toLowerCase().replace(/ /g, "_")
|
|
2118
|
+
}));
|
|
2119
|
+
}
|
|
2120
|
+
async _searchSourceNode(sourceEmbedding, userId, threshold = 0.9) {
|
|
2121
|
+
const session = this.graph.session();
|
|
2122
|
+
try {
|
|
2123
|
+
const cypher = `
|
|
2124
|
+
MATCH (source_candidate)
|
|
2125
|
+
WHERE source_candidate.embedding IS NOT NULL
|
|
2126
|
+
AND source_candidate.user_id = $user_id
|
|
2127
|
+
|
|
2128
|
+
WITH source_candidate,
|
|
2129
|
+
round(
|
|
2130
|
+
reduce(dot = 0.0, i IN range(0, size(source_candidate.embedding)-1) |
|
|
2131
|
+
dot + source_candidate.embedding[i] * $source_embedding[i]) /
|
|
2132
|
+
(sqrt(reduce(l2 = 0.0, i IN range(0, size(source_candidate.embedding)-1) |
|
|
2133
|
+
l2 + source_candidate.embedding[i] * source_candidate.embedding[i])) *
|
|
2134
|
+
sqrt(reduce(l2 = 0.0, i IN range(0, size($source_embedding)-1) |
|
|
2135
|
+
l2 + $source_embedding[i] * $source_embedding[i])))
|
|
2136
|
+
, 4) AS source_similarity
|
|
2137
|
+
WHERE source_similarity >= $threshold
|
|
2138
|
+
|
|
2139
|
+
WITH source_candidate, source_similarity
|
|
2140
|
+
ORDER BY source_similarity DESC
|
|
2141
|
+
LIMIT 1
|
|
2142
|
+
|
|
2143
|
+
RETURN elementId(source_candidate) as element_id
|
|
2144
|
+
`;
|
|
2145
|
+
const params = {
|
|
2146
|
+
source_embedding: sourceEmbedding,
|
|
2147
|
+
user_id: userId,
|
|
2148
|
+
threshold
|
|
2149
|
+
};
|
|
2150
|
+
const result = await session.run(cypher, params);
|
|
2151
|
+
return result.records.map((record) => ({
|
|
2152
|
+
elementId: record.get("element_id").toString()
|
|
2153
|
+
}));
|
|
2154
|
+
} finally {
|
|
2155
|
+
await session.close();
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
async _searchDestinationNode(destinationEmbedding, userId, threshold = 0.9) {
|
|
2159
|
+
const session = this.graph.session();
|
|
2160
|
+
try {
|
|
2161
|
+
const cypher = `
|
|
2162
|
+
MATCH (destination_candidate)
|
|
2163
|
+
WHERE destination_candidate.embedding IS NOT NULL
|
|
2164
|
+
AND destination_candidate.user_id = $user_id
|
|
2165
|
+
|
|
2166
|
+
WITH destination_candidate,
|
|
2167
|
+
round(
|
|
2168
|
+
reduce(dot = 0.0, i IN range(0, size(destination_candidate.embedding)-1) |
|
|
2169
|
+
dot + destination_candidate.embedding[i] * $destination_embedding[i]) /
|
|
2170
|
+
(sqrt(reduce(l2 = 0.0, i IN range(0, size(destination_candidate.embedding)-1) |
|
|
2171
|
+
l2 + destination_candidate.embedding[i] * destination_candidate.embedding[i])) *
|
|
2172
|
+
sqrt(reduce(l2 = 0.0, i IN range(0, size($destination_embedding)-1) |
|
|
2173
|
+
l2 + $destination_embedding[i] * $destination_embedding[i])))
|
|
2174
|
+
, 4) AS destination_similarity
|
|
2175
|
+
WHERE destination_similarity >= $threshold
|
|
2176
|
+
|
|
2177
|
+
WITH destination_candidate, destination_similarity
|
|
2178
|
+
ORDER BY destination_similarity DESC
|
|
2179
|
+
LIMIT 1
|
|
2180
|
+
|
|
2181
|
+
RETURN elementId(destination_candidate) as element_id
|
|
2182
|
+
`;
|
|
2183
|
+
const params = {
|
|
2184
|
+
destination_embedding: destinationEmbedding,
|
|
2185
|
+
user_id: userId,
|
|
2186
|
+
threshold
|
|
2187
|
+
};
|
|
2188
|
+
const result = await session.run(cypher, params);
|
|
2189
|
+
return result.records.map((record) => ({
|
|
2190
|
+
elementId: record.get("element_id").toString()
|
|
2191
|
+
}));
|
|
2192
|
+
} finally {
|
|
2193
|
+
await session.close();
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
};
|
|
2197
|
+
|
|
1410
2198
|
// src/oss/src/memory/index.ts
|
|
1411
2199
|
var Memory = class _Memory {
|
|
1412
2200
|
constructor(config = {}) {
|
|
@@ -1427,6 +2215,10 @@ var Memory = class _Memory {
|
|
|
1427
2215
|
this.db = new SQLiteManager(this.config.historyDbPath || ":memory:");
|
|
1428
2216
|
this.collectionName = this.config.vectorStore.config.collectionName;
|
|
1429
2217
|
this.apiVersion = this.config.version || "v1.0";
|
|
2218
|
+
this.enableGraph = this.config.enableGraph || false;
|
|
2219
|
+
if (this.enableGraph && this.config.graphStore) {
|
|
2220
|
+
this.graphMemory = new MemoryGraph(this.config);
|
|
2221
|
+
}
|
|
1430
2222
|
}
|
|
1431
2223
|
static fromConfig(configDict) {
|
|
1432
2224
|
try {
|
|
@@ -1460,7 +2252,21 @@ var Memory = class _Memory {
|
|
|
1460
2252
|
metadata,
|
|
1461
2253
|
filters
|
|
1462
2254
|
);
|
|
1463
|
-
|
|
2255
|
+
let graphResult;
|
|
2256
|
+
if (this.graphMemory) {
|
|
2257
|
+
try {
|
|
2258
|
+
graphResult = await this.graphMemory.add(
|
|
2259
|
+
parsedMessages.map((m) => m.content).join("\n"),
|
|
2260
|
+
filters
|
|
2261
|
+
);
|
|
2262
|
+
} catch (error) {
|
|
2263
|
+
console.error("Error adding to graph memory:", error);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
return {
|
|
2267
|
+
results: vectorStoreResult,
|
|
2268
|
+
relations: graphResult == null ? void 0 : graphResult.relations
|
|
2269
|
+
};
|
|
1464
2270
|
}
|
|
1465
2271
|
async addToVectorStore(messages, metadata, filters) {
|
|
1466
2272
|
const parsedMessages = messages.map((m) => m.content).join("\n");
|
|
@@ -1604,6 +2410,14 @@ ${parsedMessages}`] : getFactRetrievalMessages(parsedMessages);
|
|
|
1604
2410
|
limit,
|
|
1605
2411
|
filters
|
|
1606
2412
|
);
|
|
2413
|
+
let graphResults;
|
|
2414
|
+
if (this.graphMemory) {
|
|
2415
|
+
try {
|
|
2416
|
+
graphResults = await this.graphMemory.search(query, filters);
|
|
2417
|
+
} catch (error) {
|
|
2418
|
+
console.error("Error searching graph memory:", error);
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
1607
2421
|
const excludedKeys = /* @__PURE__ */ new Set([
|
|
1608
2422
|
"userId",
|
|
1609
2423
|
"agentId",
|
|
@@ -1625,7 +2439,10 @@ ${parsedMessages}`] : getFactRetrievalMessages(parsedMessages);
|
|
|
1625
2439
|
...mem.payload.agentId && { agentId: mem.payload.agentId },
|
|
1626
2440
|
...mem.payload.runId && { runId: mem.payload.runId }
|
|
1627
2441
|
}));
|
|
1628
|
-
return {
|
|
2442
|
+
return {
|
|
2443
|
+
results,
|
|
2444
|
+
relations: graphResults
|
|
2445
|
+
};
|
|
1629
2446
|
}
|
|
1630
2447
|
async update(memoryId, data) {
|
|
1631
2448
|
const embedding = await this.embedder.embed(data);
|
|
@@ -1659,6 +2476,9 @@ ${parsedMessages}`] : getFactRetrievalMessages(parsedMessages);
|
|
|
1659
2476
|
async reset() {
|
|
1660
2477
|
await this.db.reset();
|
|
1661
2478
|
await this.vectorStore.deleteCol();
|
|
2479
|
+
if (this.graphMemory) {
|
|
2480
|
+
await this.graphMemory.deleteAll({ userId: "default" });
|
|
2481
|
+
}
|
|
1662
2482
|
this.vectorStore = VectorStoreFactory.create(
|
|
1663
2483
|
this.config.vectorStore.provider,
|
|
1664
2484
|
this.config.vectorStore.config
|