workflows/test-ai-memory.json

662 lines
26 KiB
JSON

{
"createdAt": "2025-11-15T03:06:25.006Z",
"updatedAt": "2025-11-17T00:55:34.000Z",
"id": "yoXYSA197mEd6zfK",
"name": "test-ai-memory",
"active": true,
"isArchived": false,
"nodes": [
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "9d0acfc5-26eb-43da-84a4-14f2bed657c8",
"name": "user_id",
"value": "={{\n (\n {\n 'bill_a_': 'bill',\n 'bill_alt': 'bill',\n 'paddy_x': 'paddy'\n }[$json.body?.user?.name]\n )\n || $json.user_id\n || $json.userId\n || $json.body?.user?.name\n || $json.user?.id\n || 'bill'\n}}\n",
"type": "string"
},
{
"id": "7ca5458c-b3d5-4a65-b7f2-4e47a9eef4df",
"name": "message",
"value": "={{\n $json.body.content\n || $json.text\n || $json.input\n || $json.prompt\n || $json.query\n || $json.chatInput\n || \"No message found\"\n}}\n",
"type": "string"
},
{
"id": "b42f61e3-4906-4630-ab45-2731780e86bd",
"name": "sessionID",
"value": "={{ $now }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
32,
-64
],
"id": "3cd210ac-9aec-401f-b4d8-2ac0de844967",
"name": "Set Input"
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT\n role,\n content,\n created_at\nFROM user_episodes\nWHERE user_id = $1\nORDER BY created_at DESC\nLIMIT 20;\n",
"options": {
"queryReplacement": "={{ $('Set Input').item.json.user_id }}"
}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
480,
-64
],
"id": "d5c07210-d099-429c-8658-be6b6c9538fd",
"name": "Load Episodes",
"alwaysOutputData": true,
"credentials": {
"postgres": {
"id": "0kFTQeK8ZmBaEUtX",
"name": "Postgres account"
}
}
},
{
"parameters": {
"jsCode": "// Original input (user_id, message) from Set Input\nconst inputItems = $items(\"Set Input\");\nconst input = inputItems.length ? inputItems[0].json : $json;\n\n// Profile rows from Postgres\nconst profileItems = $items(\"Load Profile Memory\").map(i => i.json);\n\n// Episode rows from Postgres\nconst episodeItems = $items(\"Load Episodes\").map(i => i.json);\n\n// Build profile lines\nconst profileLines = profileItems.map(p =>\n `- ${p.key}: ${JSON.stringify(p.value)} (importance=${p.importance})`\n);\n\n// Sort episodes oldest → newest\nepisodeItems.sort(\n (a, b) => new Date(a.created_at || 0) - new Date(b.created_at || 0)\n);\n\n// Build episode lines with safe defaults\nconst episodeLines = episodeItems.map(e => {\n const role = (e.role || \"user\").toString().toUpperCase();\n const when = e.created_at || \"\";\n const content = e.content || \"\";\n return `${role} [${when}]: ${content}`;\n});\n\nconst memoryContext = `\nThe following is persistent memory about this user:\n\nProfile:\n${profileLines.length ? profileLines.join(\"\\n\") : \"- (none yet)\"}\n\nRecent interactions:\n${episodeLines.length ? episodeLines.join(\"\\n\") : \"- (none recorded)\"}\n`;\n\nreturn [\n {\n json: {\n ...input,\n memoryContext,\n },\n },\n];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
704,
-64
],
"id": "de10d931-5ed0-4728-938c-0b40d0c50020",
"name": "Build Memory Context"
},
{
"parameters": {
"promptType": "define",
"text": "={{ $json[\"message\"] }}\n",
"options": {
"systemMessage": "=You are an assistant for a specific user in a home-lab / home-automation environment.\n\nHere is what you already know about this user:\n\n {{ $json.memoryContext }}\n\nUse this information to personalize responses and maintain continuity.\n\nIf some stored information appears outdated or contradicted by the current message, prefer the latest information.\n"
}
},
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 2.2,
"position": [
928,
-64
],
"id": "d3d4f432-b6d2-4fbd-a0c9-13ce23ce6d40",
"name": "AI With Memory"
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT key, value, importance, updated_at\nFROM user_profile_memories\nWHERE user_id = '{{$json[\"user_id\"]}}'\nORDER BY importance DESC, updated_at DESC;\n",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
256,
-64
],
"id": "cf53308e-da01-45fb-b262-ef96fe032473",
"name": "Load Profile Memory",
"alwaysOutputData": true,
"retryOnFail": false,
"credentials": {
"postgres": {
"id": "0kFTQeK8ZmBaEUtX",
"name": "Postgres account"
}
}
},
{
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-mini"
},
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.2,
"position": [
1328,
528
],
"id": "56d7d2b5-5f3b-4139-9ae5-31a3534709c7",
"name": "OpenAI Chat Model",
"credentials": {
"openAiApi": {
"id": "0aLJYVCIXPIQZb1L",
"name": "OpenAi account"
}
}
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "798e8e3a-a19e-4b07-8996-69198436ee42",
"name": "user_id",
"value": "={{ $('Build Memory Context').item.json.user_id }}",
"type": "string"
},
{
"id": "9f8f1a53-bfe3-4b5c-9e7b-955319aa6f8f",
"name": "message",
"value": "={{ $json.output }}",
"type": "string"
},
{
"id": "9653c980-eba9-4b0b-8f8d-accb21b2a355",
"name": "assistant_reply",
"value": "={{ $json.output }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1312,
192
],
"id": "e213696a-a96f-4521-8cab-e04e97d01dc4",
"name": "Prepare For Memory Extraction"
},
{
"parameters": {
"jsCode": "// Get user_id / message from Prepare For Memory Extraction\nconst baseItems = $items(\"Prepare For Memory Extraction\");\nconst base = baseItems.length ? baseItems[0].json : {};\n\n\n// Get Extract Memory output\nconst extractItems = $items(\"Extract Memory\");\n\nif (!extractItems.length) {\n return [{\n json: {\n user_id: base.user_id ?? null,\n profile_upserts: [],\n episodes: [],\n debug: { reason: \"no Extract Memory items\" },\n },\n }];\n}\n\n// The Extract Memory node returns a JSON string in `output`\nconst extract = extractItems[0].json;\nlet raw = extract.output;\n\nif (typeof raw !== \"string\" || !raw.trim()) {\n // Nothing usable\n return [{\n json: {\n user_id: base.user_id ?? null,\n profile_upserts: [],\n episodes: [],\n debug: {\n reason: \"no usable string in extract.output\",\n extract,\n },\n },\n }];\n}\n\nlet parsed;\ntry {\n parsed = JSON.parse(raw);\n} catch (e) {\n return [{\n json: {\n user_id: base.user_id ?? null,\n profile_upserts: [],\n episodes: [],\n debug: {\n reason: \"JSON.parse failed\",\n error: e.message,\n raw,\n },\n },\n }];\n}\n\n// If the model returned `[ { ... } ]`, unwrap first element\nif (Array.isArray(parsed)) {\n parsed = parsed[0] || {};\n}\n\n// Pull arrays out (with sane defaults)\nconst profile_upserts = parsed.profile_upserts || [];\nconst episodes = parsed.episodes || [];\n\nreturn [{\n json: {\n user_id: base.user_id ?? null,\n profile_upserts,\n episodes,\n },\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1872,
192
],
"id": "e44777c0-56d7-44fd-bc87-5b0259d09b9c",
"name": "Parse Memory JSON"
},
{
"parameters": {
"promptType": "define",
"text": "=Conversation:\nUSER: {{ $json[\"message\"] }}\nASSISTANT: {{ $json[\"assistant_reply\"] }}\n",
"options": {
"systemMessage": "You are a memory extraction module.\n\nFrom the following conversation between user and assistant, decide what should be stored as persistent memory about the user.\n\nOnly include facts that:\n- are about the user (preferences, personal details, long-term projects, corrections, etc.)\n- will be useful in future conversations.\n\nDo NOT restate generic info or one-off particulars that will not be useful later.\n\nOutput STRICT JSON with this shape:\n\n{\n \"profile_upserts\": [\n { \"key\": \"string\", \"value\": { ... }, \"importance\": 1-5 }\n ],\n \"episodes\": [\n { \"role\": \"user\" | \"assistant\", \"content\": \"string\" }\n ]\n}\n\nIf there is nothing to store, use empty arrays:\n{\n \"profile_upserts\": [],\n \"episodes\": []\n}\n"
}
},
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 2.2,
"position": [
1536,
-16
],
"id": "2dc60f06-a015-48c2-a5d8-a9f146f7da6b",
"name": "Extract Memory"
},
{
"parameters": {
"jsCode": "const { user_id, profile_upserts } = $json;\n\nconst out = (profile_upserts || []).map(m => ({\n json: {\n user_id,\n key: m.key,\n value_serialized: JSON.stringify(m.value ?? {}),\n importance: m.importance ?? 1,\n },\n}));\n\n// If no profile memories, return an empty list (node will just do nothing next)\nreturn out;\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2112,
112
],
"id": "f5829dbd-1475-4602-ac39-a5ec4124d868",
"name": "Fan Out Profile",
"alwaysOutputData": false
},
{
"parameters": {
"jsCode": "const { user_id, episodes } = $json;\n\nconst out = (episodes || []).map(e => ({\n json: {\n user_id,\n role: e.role || \"user\",\n content: e.content || \"\",\n },\n}));\n\nreturn out;\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2112,
304
],
"id": "8e604b1b-654e-4233-ba30-988b5291f8e5",
"name": "Fan Out Episodes",
"alwaysOutputData": false
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO user_profile_memories (user_id, key, value, importance, updated_at)\nVALUES ($1, $2, $3::jsonb, $4, now())\nON CONFLICT (user_id, key)\nDO UPDATE SET\n value = EXCLUDED.value,\n importance = EXCLUDED.importance,\n updated_at = now();\n",
"options": {
"queryReplacement": "={{ $json[\"user_id\"] }}, {{ $json[\"key\"] }}, {{ $json[\"value_serialized\"] }}, {{ $json[\"importance\"] }}"
}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
2336,
112
],
"id": "45112ca7-515d-4687-a486-42e9540442bd",
"name": "Upsert Profile Memory",
"alwaysOutputData": false,
"credentials": {
"postgres": {
"id": "0kFTQeK8ZmBaEUtX",
"name": "Postgres account"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO user_episodes (user_id, role, content)\nVALUES ($1, $2, $3);\n",
"options": {
"queryReplacement": "={{ $json[\"user_id\"] }}, {{ $json[\"role\"] }}, {{ $json[\"content\"] }}"
}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
2336,
304
],
"id": "44159ab6-7b57-4e57-8550-e3e282f7fd33",
"name": "Insert Episodes",
"executeOnce": false,
"credentials": {
"postgres": {
"id": "0kFTQeK8ZmBaEUtX",
"name": "Postgres account"
}
}
},
{
"parameters": {
"httpMethod": "POST",
"path": "discord/Tahoma",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-224,
-128
],
"id": "3a28dd25-23a1-45da-8cbb-5e37cb77bce2",
"name": "discordWebhook",
"webhookId": "f9a4841a-3e54-4432-9f4f-b380cb44c6a8"
},
{
"parameters": {
"method": "POST",
"url": "http://10.20.22.1:8282/send",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "X-Tahoma-Token",
"value": "superlongrandomsharedsecret"
}
]
},
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "user_id",
"value": "={{ $('discordWebhook').first().json.body.user.id }}"
},
{
"name": "reply_to_message_id",
"value": "={{ $('discordWebhook').first().json.body.message_id }}"
},
{
"name": "content",
"value": "={{ $('AI With Memory').first().json.output }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1536,
-192
],
"id": "e532ff47-1989-4aa4-bbab-35d23124a4f8",
"name": "DM Message"
},
{
"parameters": {
"method": "POST",
"url": "http://10.20.22.1:8282/send",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "X-Tahoma-Token",
"value": "superlongrandomsharedsecret"
}
]
},
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "channel_id",
"value": "={{ $('discordWebhook').first().json.body.channel.id }}"
},
{
"name": "reply_to_message_id",
"value": "={{ $('discordWebhook').first().json.body.message_id }}"
},
{
"name": "content",
"value": "={{ $('AI With Memory').item.json.output }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1536,
-384
],
"id": "bb46440b-f3c7-4a11-8b10-55b019e5e726",
"name": "Channel Message"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"leftValue": "={{ $('discordWebhook').first().json.body.source }}",
"rightValue": "mention",
"operator": {
"type": "string",
"operation": "equals"
},
"id": "4e57debd-1943-49a9-b794-65bec790bb4f"
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "Channel"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "e0ca0788-c704-4799-8525-adff31e68a86",
"leftValue": "={{ $('discordWebhook').first().json.body.source }}",
"rightValue": "dm",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "User"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.3,
"position": [
1312,
-288
],
"id": "d22ac00e-9205-4260-87ea-c1e715e6e0dd",
"name": "Response Channel",
"executeOnce": false
}
],
"connections": {
"Set Input": {
"main": [
[
{
"node": "Load Profile Memory",
"type": "main",
"index": 0
}
]
]
},
"Load Episodes": {
"main": [
[
{
"node": "Build Memory Context",
"type": "main",
"index": 0
}
]
]
},
"Build Memory Context": {
"main": [
[
{
"node": "AI With Memory",
"type": "main",
"index": 0
}
]
]
},
"Load Profile Memory": {
"main": [
[
{
"node": "Load Episodes",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Chat Model": {
"ai_languageModel": [
[
{
"node": "AI With Memory",
"type": "ai_languageModel",
"index": 0
},
{
"node": "Extract Memory",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"AI With Memory": {
"main": [
[
{
"node": "Prepare For Memory Extraction",
"type": "main",
"index": 0
},
{
"node": "Response Channel",
"type": "main",
"index": 0
}
]
]
},
"Prepare For Memory Extraction": {
"main": [
[
{
"node": "Extract Memory",
"type": "main",
"index": 0
},
{
"node": "Parse Memory JSON",
"type": "main",
"index": 0
}
]
]
},
"Extract Memory": {
"main": [
[
{
"node": "Parse Memory JSON",
"type": "main",
"index": 0
}
]
]
},
"Parse Memory JSON": {
"main": [
[
{
"node": "Fan Out Episodes",
"type": "main",
"index": 0
},
{
"node": "Fan Out Profile",
"type": "main",
"index": 0
}
]
]
},
"Fan Out Episodes": {
"main": [
[
{
"node": "Insert Episodes",
"type": "main",
"index": 0
}
]
]
},
"Fan Out Profile": {
"main": [
[
{
"node": "Upsert Profile Memory",
"type": "main",
"index": 0
}
]
]
},
"Insert Episodes": {
"main": [
[]
]
},
"discordWebhook": {
"main": [
[
{
"node": "Set Input",
"type": "main",
"index": 0
}
]
]
},
"Upsert Profile Memory": {
"main": [
[]
]
},
"Response Channel": {
"main": [
[
{
"node": "Channel Message",
"type": "main",
"index": 0
}
],
[
{
"node": "DM Message",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"meta": {
"templateCredsSetupCompleted": true
},
"pinData": {},
"versionId": "c5484de0-f952-417a-bf0c-1f6acb4afe27",
"triggerCount": 1,
"shared": [
{
"createdAt": "2025-11-15T03:06:25.010Z",
"updatedAt": "2025-11-15T03:06:25.010Z",
"role": "workflow:owner",
"workflowId": "yoXYSA197mEd6zfK",
"projectId": "oI3LZpkceKxAFXfg"
}
],
"tags": []
}