@tpsdev-ai/n8n-nodes-flair 0.8.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/README.md +33 -0
- package/dist/credentials/FlairApi.credentials.d.ts +30 -0
- package/dist/credentials/FlairApi.credentials.js +71 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/nodes/.gitkeep +0 -0
- package/dist/nodes/FlairChatMemory/FlairChatMemory.node.d.ts +5 -0
- package/dist/nodes/FlairChatMemory/FlairChatMemory.node.js +91 -0
- package/dist/nodes/FlairChatMemory/FlairChatMessageHistory.d.ts +34 -0
- package/dist/nodes/FlairChatMemory/FlairChatMessageHistory.js +99 -0
- package/dist/nodes/FlairSearch/FlairSearch.node.d.ts +6 -0
- package/dist/nodes/FlairSearch/FlairSearch.node.js +182 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @tpsdev-ai/n8n-nodes-flair
|
|
2
|
+
|
|
3
|
+
n8n community node — use [Flair](https://github.com/tpsdev-ai/flair) as your AI Agent's memory backend.
|
|
4
|
+
|
|
5
|
+
## Nodes
|
|
6
|
+
|
|
7
|
+
- **Flair Chat Memory** — n8n AI Agent Memory port. Stores chat history in Flair, replayable across runs and readable from Claude Code, OpenClaw, and any other Flair client. LangChain `BufferWindowMemory` under the hood.
|
|
8
|
+
- **Flair Search** — n8n AI Agent Tool port. Two operations:
|
|
9
|
+
- *Semantic Search* — finds memories ranked by similarity to a natural-language query.
|
|
10
|
+
- *Get By Subject* — lists memories filtered by subject, ordered by recency.
|
|
11
|
+
- *Get By Tag* — coming in a follow-up once `flair-client.memory.list` exposes a `tags` filter (tracked in the [spec](https://github.com/tpsdev-ai/flair/blob/main/specs/N8N-NODE-q3qf.md) §6).
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install @tpsdev-ai/n8n-nodes-flair
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then restart your n8n instance. The Flair API credential will appear under **Credentials** → **New** → **Flair API**.
|
|
20
|
+
|
|
21
|
+
Full setup walkthrough, subject/sessionId patterns, and security guidance are in [`docs/n8n.md`](https://github.com/tpsdev-ai/flair/blob/main/docs/n8n.md) in the Flair repo.
|
|
22
|
+
|
|
23
|
+
## Credential setup
|
|
24
|
+
|
|
25
|
+
1. **Base URL** — your Flair instance, e.g. `http://localhost:9926`
|
|
26
|
+
2. **Agent ID** — the logical identity that will own memories written from this n8n workspace. Workflows that share an Agent ID share memory ownership.
|
|
27
|
+
3. **Admin Password** — your Flair (Harper) admin password. **This grants read/write access to the entire instance.** For production with untrusted workflow inputs, wait for Ed25519 per-agent auth (planned).
|
|
28
|
+
|
|
29
|
+
The credential test hits `/Memory` (auth-required) — you'll know it works when the test succeeds.
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
Apache-2.0
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { IAuthenticateGeneric, ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
2
|
+
/**
|
|
3
|
+
* Flair API credential — v1 Harper admin-password authentication.
|
|
4
|
+
*
|
|
5
|
+
* SECURITY NOTE: the admin password grants read/write to the entire Flair
|
|
6
|
+
* instance, not just the specified agentId. The blast radius is the whole
|
|
7
|
+
* memory store. This is acceptable for v1 / proof-of-concept where the
|
|
8
|
+
* operator controls the n8n workflow inputs, but planned Ed25519 per-agent
|
|
9
|
+
* auth should land before any deployment with sensitive memories from
|
|
10
|
+
* untrusted workflow inputs.
|
|
11
|
+
*
|
|
12
|
+
* The agentId field controls memory ownership — workflows that share an
|
|
13
|
+
* agentId share memory ownership, allowing "this assistant remembers"
|
|
14
|
+
* patterns across workflows. Use distinct agentIds when isolation matters.
|
|
15
|
+
*
|
|
16
|
+
* AUTH FLOW: Flair (Harper) accepts `Authorization: Basic` with the
|
|
17
|
+
* admin user (`admin`) and the admin password. We pass the password in
|
|
18
|
+
* via the `adminPassword` credential field; the Basic-auth header is
|
|
19
|
+
* constructed via the n8n expression engine. The credential test hits
|
|
20
|
+
* `/Memory` (auth-required) — `/Health` is unauthenticated, so testing
|
|
21
|
+
* against it would silently pass with wrong credentials.
|
|
22
|
+
*/
|
|
23
|
+
export declare class FlairApi implements ICredentialType {
|
|
24
|
+
name: string;
|
|
25
|
+
displayName: string;
|
|
26
|
+
documentationUrl: string;
|
|
27
|
+
properties: INodeProperties[];
|
|
28
|
+
authenticate: IAuthenticateGeneric;
|
|
29
|
+
test: ICredentialTestRequest;
|
|
30
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FlairApi = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Flair API credential — v1 Harper admin-password authentication.
|
|
6
|
+
*
|
|
7
|
+
* SECURITY NOTE: the admin password grants read/write to the entire Flair
|
|
8
|
+
* instance, not just the specified agentId. The blast radius is the whole
|
|
9
|
+
* memory store. This is acceptable for v1 / proof-of-concept where the
|
|
10
|
+
* operator controls the n8n workflow inputs, but planned Ed25519 per-agent
|
|
11
|
+
* auth should land before any deployment with sensitive memories from
|
|
12
|
+
* untrusted workflow inputs.
|
|
13
|
+
*
|
|
14
|
+
* The agentId field controls memory ownership — workflows that share an
|
|
15
|
+
* agentId share memory ownership, allowing "this assistant remembers"
|
|
16
|
+
* patterns across workflows. Use distinct agentIds when isolation matters.
|
|
17
|
+
*
|
|
18
|
+
* AUTH FLOW: Flair (Harper) accepts `Authorization: Basic` with the
|
|
19
|
+
* admin user (`admin`) and the admin password. We pass the password in
|
|
20
|
+
* via the `adminPassword` credential field; the Basic-auth header is
|
|
21
|
+
* constructed via the n8n expression engine. The credential test hits
|
|
22
|
+
* `/Memory` (auth-required) — `/Health` is unauthenticated, so testing
|
|
23
|
+
* against it would silently pass with wrong credentials.
|
|
24
|
+
*/
|
|
25
|
+
class FlairApi {
|
|
26
|
+
name = 'flairApi';
|
|
27
|
+
displayName = 'Flair API';
|
|
28
|
+
documentationUrl = 'https://github.com/tpsdev-ai/flair#n8n';
|
|
29
|
+
properties = [
|
|
30
|
+
{
|
|
31
|
+
displayName: 'Base URL',
|
|
32
|
+
name: 'baseUrl',
|
|
33
|
+
type: 'string',
|
|
34
|
+
default: 'http://localhost:9926',
|
|
35
|
+
required: true,
|
|
36
|
+
description: 'The Flair instance URL. Use http://localhost:9926 for local installs.',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
displayName: 'Agent ID',
|
|
40
|
+
name: 'agentId',
|
|
41
|
+
type: 'string',
|
|
42
|
+
default: '',
|
|
43
|
+
required: true,
|
|
44
|
+
description: 'Logical identity used as the memory owner. Workflows that share an agentId share memory ownership.',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
displayName: 'Admin Password',
|
|
48
|
+
name: 'adminPassword',
|
|
49
|
+
type: 'string',
|
|
50
|
+
typeOptions: { password: true },
|
|
51
|
+
default: '',
|
|
52
|
+
required: true,
|
|
53
|
+
description: "Flair (Harper) admin password. Sensitive: grants read/write to the entire instance. Use Ed25519 per-agent auth (planned) for production with untrusted workflow inputs.",
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
authenticate = {
|
|
57
|
+
type: 'generic',
|
|
58
|
+
properties: {
|
|
59
|
+
headers: {
|
|
60
|
+
Authorization: "=Basic {{ Buffer.from('admin:' + $credentials.adminPassword).toString('base64') }}",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
test = {
|
|
65
|
+
request: {
|
|
66
|
+
baseURL: '={{ $credentials.baseUrl }}',
|
|
67
|
+
url: '/Memory',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
exports.FlairApi = FlairApi;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
File without changes
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type ISupplyDataFunctions, type INodeType, type INodeTypeDescription, type SupplyData } from "n8n-workflow";
|
|
2
|
+
export declare class FlairChatMemory implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FlairChatMemory = void 0;
|
|
4
|
+
const memory_1 = require("@langchain/classic/memory");
|
|
5
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
6
|
+
const flair_client_1 = require("@tpsdev-ai/flair-client");
|
|
7
|
+
const FlairChatMessageHistory_1 = require("./FlairChatMessageHistory");
|
|
8
|
+
class FlairChatMemory {
|
|
9
|
+
description = {
|
|
10
|
+
displayName: "Flair Chat Memory",
|
|
11
|
+
name: "flairChatMemory",
|
|
12
|
+
icon: "file:flair.svg",
|
|
13
|
+
group: ["transform"],
|
|
14
|
+
version: [1],
|
|
15
|
+
description: "Store n8n AI Agent chat history in Flair, a portable agent memory backend. The same memory is readable from Claude Code, OpenClaw, and any other Flair client.",
|
|
16
|
+
defaults: {
|
|
17
|
+
name: "Flair Chat Memory",
|
|
18
|
+
},
|
|
19
|
+
credentials: [
|
|
20
|
+
{
|
|
21
|
+
name: "flairApi",
|
|
22
|
+
required: true,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
codex: {
|
|
26
|
+
categories: ["AI"],
|
|
27
|
+
subcategories: {
|
|
28
|
+
AI: ["Memory"],
|
|
29
|
+
Memory: ["Other memories"],
|
|
30
|
+
},
|
|
31
|
+
resources: {
|
|
32
|
+
primaryDocumentation: [
|
|
33
|
+
{
|
|
34
|
+
url: "https://github.com/tpsdev-ai/flair#n8n",
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
inputs: [],
|
|
40
|
+
outputs: [n8n_workflow_1.NodeConnectionTypes.AiMemory],
|
|
41
|
+
outputNames: ["Memory"],
|
|
42
|
+
properties: [
|
|
43
|
+
{
|
|
44
|
+
displayName: "Subject",
|
|
45
|
+
name: "subject",
|
|
46
|
+
type: "string",
|
|
47
|
+
default: '={{ $workflow.name }}',
|
|
48
|
+
required: true,
|
|
49
|
+
description: "Flair subject that scopes the chat history. Defaults to the workflow name. Workflows that share a subject share memory.",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
displayName: "Session Sub-Key",
|
|
53
|
+
name: "sessionKey",
|
|
54
|
+
type: "string",
|
|
55
|
+
default: "",
|
|
56
|
+
description: "Optional sub-scope appended to the subject as `<subject>:<sessionKey>`. Use the n8n execution id (`={{ $execution.id }}`) for per-run isolation, or leave blank to share memory across runs.",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
displayName: "Context Window Length",
|
|
60
|
+
name: "contextWindowLength",
|
|
61
|
+
type: "number",
|
|
62
|
+
default: 10,
|
|
63
|
+
description: "How many message turns (user + AI = 1 turn) to keep in the buffer window.",
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
async supplyData(itemIndex) {
|
|
68
|
+
const credentials = (await this.getCredentials("flairApi"));
|
|
69
|
+
const subject = this.getNodeParameter("subject", itemIndex);
|
|
70
|
+
const sessionKey = this.getNodeParameter("sessionKey", itemIndex, "");
|
|
71
|
+
const k = this.getNodeParameter("contextWindowLength", itemIndex, 10);
|
|
72
|
+
const composedSubject = sessionKey ? `${subject}:${sessionKey}` : subject;
|
|
73
|
+
const flair = new flair_client_1.FlairClient({
|
|
74
|
+
url: credentials.baseUrl,
|
|
75
|
+
agentId: credentials.agentId,
|
|
76
|
+
adminUser: "admin",
|
|
77
|
+
adminPassword: credentials.adminPassword,
|
|
78
|
+
});
|
|
79
|
+
const history = new FlairChatMessageHistory_1.FlairChatMessageHistory(flair, composedSubject, k);
|
|
80
|
+
const memory = new memory_1.BufferWindowMemory({
|
|
81
|
+
memoryKey: "chat_history",
|
|
82
|
+
chatHistory: history,
|
|
83
|
+
returnMessages: true,
|
|
84
|
+
inputKey: "input",
|
|
85
|
+
outputKey: "output",
|
|
86
|
+
k,
|
|
87
|
+
});
|
|
88
|
+
return { response: memory };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.FlairChatMemory = FlairChatMemory;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { BaseListChatMessageHistory } from "@langchain/core/chat_history";
|
|
2
|
+
import { type BaseMessage } from "@langchain/core/messages";
|
|
3
|
+
import type { FlairClient } from "@tpsdev-ai/flair-client";
|
|
4
|
+
/**
|
|
5
|
+
* LangChain `BaseListChatMessageHistory` adapter backed by Flair memory.
|
|
6
|
+
*
|
|
7
|
+
* Each chat message is stored as a single Flair memory:
|
|
8
|
+
* - content: JSON.stringify(StoredMessage)
|
|
9
|
+
* - subject: the composed chat-session subject
|
|
10
|
+
* - tags: ["n8n-chat", `role:<type>`]
|
|
11
|
+
* - type: "session"
|
|
12
|
+
* - durability: "ephemeral"
|
|
13
|
+
*
|
|
14
|
+
* `getMessages()` reads memories filtered by subject and reconstructs the
|
|
15
|
+
* BaseMessage[] in createdAt-ascending order. Order is computed client-side
|
|
16
|
+
* from the createdAt timestamp because Memory.list does not yet expose a
|
|
17
|
+
* server-side sort param (tracked as the `order` extension in q3qf §6).
|
|
18
|
+
*
|
|
19
|
+
* The `windowK` constructor argument bounds the number of memories fetched
|
|
20
|
+
* per `getMessages()` call: each turn is two messages (user + AI), so we
|
|
21
|
+
* fetch up to `windowK * 2` to cover a windowing wrapper that keeps the
|
|
22
|
+
* last K turns.
|
|
23
|
+
*/
|
|
24
|
+
export declare class FlairChatMessageHistory extends BaseListChatMessageHistory {
|
|
25
|
+
private readonly client;
|
|
26
|
+
private readonly subject;
|
|
27
|
+
private readonly windowK;
|
|
28
|
+
lc_namespace: string[];
|
|
29
|
+
constructor(client: FlairClient, subject: string, windowK?: number);
|
|
30
|
+
getMessages(): Promise<BaseMessage[]>;
|
|
31
|
+
addMessage(message: BaseMessage): Promise<void>;
|
|
32
|
+
addMessages(messages: BaseMessage[]): Promise<void>;
|
|
33
|
+
clear(): Promise<void>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FlairChatMessageHistory = void 0;
|
|
4
|
+
const chat_history_1 = require("@langchain/core/chat_history");
|
|
5
|
+
const messages_1 = require("@langchain/core/messages");
|
|
6
|
+
/**
|
|
7
|
+
* LangChain `BaseListChatMessageHistory` adapter backed by Flair memory.
|
|
8
|
+
*
|
|
9
|
+
* Each chat message is stored as a single Flair memory:
|
|
10
|
+
* - content: JSON.stringify(StoredMessage)
|
|
11
|
+
* - subject: the composed chat-session subject
|
|
12
|
+
* - tags: ["n8n-chat", `role:<type>`]
|
|
13
|
+
* - type: "session"
|
|
14
|
+
* - durability: "ephemeral"
|
|
15
|
+
*
|
|
16
|
+
* `getMessages()` reads memories filtered by subject and reconstructs the
|
|
17
|
+
* BaseMessage[] in createdAt-ascending order. Order is computed client-side
|
|
18
|
+
* from the createdAt timestamp because Memory.list does not yet expose a
|
|
19
|
+
* server-side sort param (tracked as the `order` extension in q3qf §6).
|
|
20
|
+
*
|
|
21
|
+
* The `windowK` constructor argument bounds the number of memories fetched
|
|
22
|
+
* per `getMessages()` call: each turn is two messages (user + AI), so we
|
|
23
|
+
* fetch up to `windowK * 2` to cover a windowing wrapper that keeps the
|
|
24
|
+
* last K turns.
|
|
25
|
+
*/
|
|
26
|
+
class FlairChatMessageHistory extends chat_history_1.BaseListChatMessageHistory {
|
|
27
|
+
client;
|
|
28
|
+
subject;
|
|
29
|
+
windowK;
|
|
30
|
+
lc_namespace = ["n8n-nodes", "flair"];
|
|
31
|
+
constructor(client, subject, windowK = 10) {
|
|
32
|
+
super();
|
|
33
|
+
this.client = client;
|
|
34
|
+
this.subject = subject;
|
|
35
|
+
this.windowK = windowK;
|
|
36
|
+
}
|
|
37
|
+
async getMessages() {
|
|
38
|
+
const memories = await this.client.memory.list({
|
|
39
|
+
subject: this.subject,
|
|
40
|
+
type: "session",
|
|
41
|
+
limit: this.windowK * 2,
|
|
42
|
+
});
|
|
43
|
+
// Server returns most recent first by default; sort by createdAt asc
|
|
44
|
+
// so chat history reads chronologically (oldest first).
|
|
45
|
+
const sorted = [...memories].sort((a, b) => {
|
|
46
|
+
const ta = a.createdAt ?? "";
|
|
47
|
+
const tb = b.createdAt ?? "";
|
|
48
|
+
return ta < tb ? -1 : ta > tb ? 1 : 0;
|
|
49
|
+
});
|
|
50
|
+
const stored = sorted
|
|
51
|
+
.map((m) => {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(m.content);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Memory content isn't a valid StoredMessage envelope — skip
|
|
57
|
+
// rather than throw. Most likely a memory written by another
|
|
58
|
+
// surface (CLI, MCP, another agent) sharing the subject.
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
.filter((s) => s !== null);
|
|
63
|
+
return (0, messages_1.mapStoredMessagesToChatMessages)(stored);
|
|
64
|
+
}
|
|
65
|
+
async addMessage(message) {
|
|
66
|
+
const [stored] = (0, messages_1.mapChatMessagesToStoredMessages)([message]);
|
|
67
|
+
await this.client.memory.write(JSON.stringify(stored), {
|
|
68
|
+
type: "session",
|
|
69
|
+
durability: "ephemeral",
|
|
70
|
+
subject: this.subject,
|
|
71
|
+
tags: ["n8n-chat", `role:${stored.type}`],
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async addMessages(messages) {
|
|
75
|
+
// Flair has no batch-write today; fall back to sequential writes.
|
|
76
|
+
// BaseListChatMessageHistory's default does the same loop — the
|
|
77
|
+
// override exists so future flair-client batch support can drop in
|
|
78
|
+
// here without touching consumers.
|
|
79
|
+
for (const m of messages)
|
|
80
|
+
await this.addMessage(m);
|
|
81
|
+
}
|
|
82
|
+
async clear() {
|
|
83
|
+
// Best-effort delete of all session memories under this subject.
|
|
84
|
+
const memories = await this.client.memory.list({
|
|
85
|
+
subject: this.subject,
|
|
86
|
+
type: "session",
|
|
87
|
+
limit: 1000,
|
|
88
|
+
});
|
|
89
|
+
for (const m of memories) {
|
|
90
|
+
try {
|
|
91
|
+
await this.client.memory.delete(m.id);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// best-effort — keep deleting siblings
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
exports.FlairChatMessageHistory = FlairChatMessageHistory;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type IExecuteFunctions, type INodeExecutionData, type INodeType, type INodeTypeDescription, type ISupplyDataFunctions, type SupplyData } from "n8n-workflow";
|
|
2
|
+
export declare class FlairSearch implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData>;
|
|
5
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FlairSearch = void 0;
|
|
4
|
+
const tools_1 = require("@langchain/core/tools");
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
7
|
+
const flair_client_1 = require("@tpsdev-ai/flair-client");
|
|
8
|
+
function makeClient(credentials) {
|
|
9
|
+
return new flair_client_1.FlairClient({
|
|
10
|
+
url: credentials.baseUrl,
|
|
11
|
+
agentId: credentials.agentId,
|
|
12
|
+
adminUser: "admin",
|
|
13
|
+
adminPassword: credentials.adminPassword,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
async function runSearch(flair, query, limit) {
|
|
17
|
+
const results = await flair.memory.search(query, { limit });
|
|
18
|
+
return results.map((r) => ({
|
|
19
|
+
id: r.id,
|
|
20
|
+
content: r.content,
|
|
21
|
+
score: r.score,
|
|
22
|
+
type: r.type,
|
|
23
|
+
tags: r.tags,
|
|
24
|
+
createdAt: r.createdAt,
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
async function runGetBySubject(flair, subject, limit) {
|
|
28
|
+
const memories = await flair.memory.list({ subject, limit });
|
|
29
|
+
return memories.map((m) => ({
|
|
30
|
+
id: m.id,
|
|
31
|
+
content: m.content,
|
|
32
|
+
type: m.type,
|
|
33
|
+
tags: m.tags,
|
|
34
|
+
subject: m.subject,
|
|
35
|
+
createdAt: m.createdAt,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
class FlairSearch {
|
|
39
|
+
description = {
|
|
40
|
+
displayName: "Flair Search",
|
|
41
|
+
name: "flairSearch",
|
|
42
|
+
icon: "file:flair.svg",
|
|
43
|
+
group: ["transform"],
|
|
44
|
+
version: [1],
|
|
45
|
+
description: "Search Flair memory by semantic query or by subject. Use as an AI Agent tool to give the agent access to structured Flair memories.",
|
|
46
|
+
defaults: {
|
|
47
|
+
name: "Flair Search",
|
|
48
|
+
},
|
|
49
|
+
credentials: [
|
|
50
|
+
{
|
|
51
|
+
name: "flairApi",
|
|
52
|
+
required: true,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
codex: {
|
|
56
|
+
categories: ["AI"],
|
|
57
|
+
subcategories: {
|
|
58
|
+
AI: ["Tools"],
|
|
59
|
+
Tools: ["Other Tools"],
|
|
60
|
+
},
|
|
61
|
+
resources: {
|
|
62
|
+
primaryDocumentation: [
|
|
63
|
+
{
|
|
64
|
+
url: "https://github.com/tpsdev-ai/flair#n8n",
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
inputs: [],
|
|
70
|
+
outputs: [n8n_workflow_1.NodeConnectionTypes.AiTool],
|
|
71
|
+
outputNames: ["Tool"],
|
|
72
|
+
properties: [
|
|
73
|
+
{
|
|
74
|
+
displayName: "Operation",
|
|
75
|
+
name: "operation",
|
|
76
|
+
type: "options",
|
|
77
|
+
noDataExpression: true,
|
|
78
|
+
options: [
|
|
79
|
+
{
|
|
80
|
+
name: "Semantic Search",
|
|
81
|
+
value: "search",
|
|
82
|
+
description: "Find memories most semantically similar to a query",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "Get By Subject",
|
|
86
|
+
value: "getBySubject",
|
|
87
|
+
description: "List memories filtered by subject (the entity/conversation/topic they're about)",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
default: "search",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
displayName: "Query",
|
|
94
|
+
name: "query",
|
|
95
|
+
type: "string",
|
|
96
|
+
default: "",
|
|
97
|
+
required: true,
|
|
98
|
+
displayOptions: { show: { operation: ["search"] } },
|
|
99
|
+
description: "The semantic query to search Flair memory for",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
displayName: "Subject",
|
|
103
|
+
name: "subject",
|
|
104
|
+
type: "string",
|
|
105
|
+
default: "",
|
|
106
|
+
required: true,
|
|
107
|
+
displayOptions: { show: { operation: ["getBySubject"] } },
|
|
108
|
+
description: "The subject (entity / topic) to fetch memories for",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
displayName: "Limit",
|
|
112
|
+
name: "limit",
|
|
113
|
+
type: "number",
|
|
114
|
+
default: 5,
|
|
115
|
+
description: "Maximum number of memories to return",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
displayName: "Get By Tag is not yet available — flair-client.memory.list does not yet expose a tag filter (tracked in q3qf spec §6). Workaround: use Semantic Search and let the model filter results by tags in the response.",
|
|
119
|
+
name: "tagNotice",
|
|
120
|
+
type: "notice",
|
|
121
|
+
default: "",
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
async supplyData(itemIndex) {
|
|
126
|
+
const credentials = (await this.getCredentials("flairApi"));
|
|
127
|
+
const operation = this.getNodeParameter("operation", itemIndex);
|
|
128
|
+
const limit = this.getNodeParameter("limit", itemIndex, 5);
|
|
129
|
+
const flair = makeClient(credentials);
|
|
130
|
+
if (operation === "search") {
|
|
131
|
+
const tool = new tools_1.DynamicStructuredTool({
|
|
132
|
+
name: "flair_search",
|
|
133
|
+
description: "Search Flair memory by semantic similarity. Returns memories ranked by relevance to the query.",
|
|
134
|
+
schema: zod_1.z.object({
|
|
135
|
+
query: zod_1.z
|
|
136
|
+
.string()
|
|
137
|
+
.describe("The natural-language query to search memory for"),
|
|
138
|
+
}),
|
|
139
|
+
func: async ({ query }) => {
|
|
140
|
+
const results = await runSearch(flair, query, limit);
|
|
141
|
+
return JSON.stringify(results);
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
return { response: tool };
|
|
145
|
+
}
|
|
146
|
+
// getBySubject — subject is bound at config time so the agent only
|
|
147
|
+
// chooses to invoke it (not which subject to fetch).
|
|
148
|
+
const subject = this.getNodeParameter("subject", itemIndex);
|
|
149
|
+
const tool = new tools_1.DynamicStructuredTool({
|
|
150
|
+
name: "flair_get_by_subject",
|
|
151
|
+
description: `Get memories about subject "${subject}" from Flair, ordered by recency.`,
|
|
152
|
+
schema: zod_1.z.object({}),
|
|
153
|
+
func: async () => {
|
|
154
|
+
const results = await runGetBySubject(flair, subject, limit);
|
|
155
|
+
return JSON.stringify(results);
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
return { response: tool };
|
|
159
|
+
}
|
|
160
|
+
async execute() {
|
|
161
|
+
const credentials = (await this.getCredentials("flairApi"));
|
|
162
|
+
const flair = makeClient(credentials);
|
|
163
|
+
const inputs = this.getInputData();
|
|
164
|
+
const out = [];
|
|
165
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
166
|
+
const operation = this.getNodeParameter("operation", i);
|
|
167
|
+
const limit = this.getNodeParameter("limit", i, 5);
|
|
168
|
+
let results;
|
|
169
|
+
if (operation === "search") {
|
|
170
|
+
const query = this.getNodeParameter("query", i);
|
|
171
|
+
results = await runSearch(flair, query, limit);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const subject = this.getNodeParameter("subject", i);
|
|
175
|
+
results = await runGetBySubject(flair, subject, limit);
|
|
176
|
+
}
|
|
177
|
+
out.push({ json: { operation, results }, pairedItem: { item: i } });
|
|
178
|
+
}
|
|
179
|
+
return [out];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
exports.FlairSearch = FlairSearch;
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tpsdev-ai/n8n-nodes-flair",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "n8n community node — use Flair as your AI Agent's memory backend. Includes FlairChatMemory (Memory port) and FlairSearch (Tool port).",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/",
|
|
9
|
+
"LICENSE",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc && npm run build:assets",
|
|
14
|
+
"build:assets": "node -e \"require('fs').cpSync('src/credentials','dist/credentials',{recursive:true,filter:p=>!p.endsWith('.ts')});require('fs').cpSync('src/nodes','dist/nodes',{recursive:true,filter:p=>!p.endsWith('.ts')})\"",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"n8n": {
|
|
21
|
+
"n8nNodesApiVersion": 1,
|
|
22
|
+
"credentials": [
|
|
23
|
+
"dist/credentials/FlairApi.credentials.js"
|
|
24
|
+
],
|
|
25
|
+
"nodes": [
|
|
26
|
+
"dist/nodes/FlairChatMemory/FlairChatMemory.node.js",
|
|
27
|
+
"dist/nodes/FlairSearch/FlairSearch.node.js"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"license": "Apache-2.0",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/tpsdev-ai/flair.git",
|
|
37
|
+
"directory": "packages/n8n-nodes-flair"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://tps.dev/#flair",
|
|
40
|
+
"keywords": [
|
|
41
|
+
"n8n-community-node-package",
|
|
42
|
+
"n8n",
|
|
43
|
+
"flair",
|
|
44
|
+
"ai",
|
|
45
|
+
"agent",
|
|
46
|
+
"memory",
|
|
47
|
+
"langchain"
|
|
48
|
+
],
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@tpsdev-ai/flair-client": "0.8.0",
|
|
51
|
+
"zod": "3.25.76"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"n8n-workflow": "*",
|
|
55
|
+
"@langchain/core": "*",
|
|
56
|
+
"@langchain/classic": "*"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"n8n-workflow": "1.119.0",
|
|
60
|
+
"@langchain/core": "1.1.44",
|
|
61
|
+
"@langchain/classic": "1.0.32",
|
|
62
|
+
"typescript": "5.9.3"
|
|
63
|
+
}
|
|
64
|
+
}
|