@wappy_ai/connector-cognee 0.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/LICENSE +21 -0
- package/dist/client.d.ts +57 -0
- package/dist/client.js +117 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/knowledge.d.ts +17 -0
- package/dist/knowledge.js +60 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 csr1010
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A hand-rolled TypeScript REST client for Cognee's real API — no official Node.js/TypeScript SDK
|
|
3
|
+
* exists (confirmed via real research: Cognee's primary SDK is Python; the REST API is the actual
|
|
4
|
+
* integration surface for this ecosystem), so this follows the same pattern already proven in
|
|
5
|
+
* `@wappy_ai/connector-google`'s `oauth.ts` (a plain `fetch`-based client, injectable `fetchImpl` for
|
|
6
|
+
* testing, no SDK dependency).
|
|
7
|
+
*
|
|
8
|
+
* Every request/response shape here was confirmed against a REAL, locally-booted Cognee server
|
|
9
|
+
* (v1.6.2) — its own live OpenAPI spec (`GET /openapi.json`) plus real `curl` round-trips (register →
|
|
10
|
+
* login → create an API key → add → cognify → search → delete), not just read from docs. Concretely
|
|
11
|
+
* confirmed, not assumed:
|
|
12
|
+
* - `POST /api/v1/add` is `multipart/form-data`, fields `raw_data` (string) + `datasetName` — a JSON
|
|
13
|
+
* body is rejected.
|
|
14
|
+
* - `POST /api/v1/cognify` is JSON `{ datasets: [name] }`.
|
|
15
|
+
* - `POST /api/v1/search` is JSON `{ query, searchType, topK? }`. `searchType` matters a lot —
|
|
16
|
+
* the default (`HYBRID_COMPLETION`) returns an LLM-composed answer, not scored chunks; `"CHUNKS"`
|
|
17
|
+
* is what returns a flat array of `{ id, text, score, document_id, document_name, ... }` objects,
|
|
18
|
+
* which is the shape this client maps onto `Knowledge`'s `RecalledChunk`.
|
|
19
|
+
* - Self-hosted auth (when enabled) uses `X-Api-Key: <key>`, NOT `Authorization: Bearer`.
|
|
20
|
+
* - `DELETE /api/v1/datasets` (no path segment) deletes **every** dataset the caller can see,
|
|
21
|
+
* confirmed by a real call — there is no "delete by name in the body" variant. Deleting one
|
|
22
|
+
* dataset requires looking its id up by name via `GET /api/v1/datasets` first, then
|
|
23
|
+
* `DELETE /api/v1/datasets/{id}`.
|
|
24
|
+
*/
|
|
25
|
+
export type FetchImpl = typeof fetch;
|
|
26
|
+
export interface CogneeClientOptions {
|
|
27
|
+
/** Your own Cognee instance — self-hosted (e.g. "http://localhost:8000") or Cognee Cloud. */
|
|
28
|
+
baseUrl: string;
|
|
29
|
+
/** Only needed for Cognee Cloud or an auth-enabled self-hosted instance. Sent as
|
|
30
|
+
* `X-Api-Key: {apiKey}`; omitted entirely for an unauthenticated self-hosted instance. */
|
|
31
|
+
apiKey?: string;
|
|
32
|
+
fetchImpl?: FetchImpl;
|
|
33
|
+
}
|
|
34
|
+
export interface CogneeChunk {
|
|
35
|
+
id?: string;
|
|
36
|
+
text?: string;
|
|
37
|
+
score?: number;
|
|
38
|
+
document_id?: string;
|
|
39
|
+
document_name?: string;
|
|
40
|
+
[key: string]: unknown;
|
|
41
|
+
}
|
|
42
|
+
export interface CogneeClient {
|
|
43
|
+
/** Ingests raw text into the dataset named `datasetName` (Cognee's own "dataset" concept — used
|
|
44
|
+
* here as the equivalent of `Knowledge`'s `sourceId`). Data added this way isn't searchable until
|
|
45
|
+
* `cognify()` runs on it. */
|
|
46
|
+
add(datasetName: string, text: string): Promise<void>;
|
|
47
|
+
/** Triggers Cognee's entity/relationship extraction pipeline over previously-`add`ed data for
|
|
48
|
+
* `datasetName`. Must run after `add`, before that data is searchable via `search()`. */
|
|
49
|
+
cognify(datasetName: string): Promise<void>;
|
|
50
|
+
/** Semantic search across ingested data, using Cognee's "CHUNKS" search type so results come back
|
|
51
|
+
* as scored passages rather than an LLM-composed answer. */
|
|
52
|
+
search(query: string, topK?: number): Promise<CogneeChunk[]>;
|
|
53
|
+
/** Removes the dataset named `datasetName` entirely. A no-op if no dataset with that name exists
|
|
54
|
+
* (never falls back to Cognee's bare `DELETE /datasets`, which deletes everything). */
|
|
55
|
+
removeDataset(datasetName: string): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
export declare function createCogneeClient(opts: CogneeClientOptions): CogneeClient;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A hand-rolled TypeScript REST client for Cognee's real API — no official Node.js/TypeScript SDK
|
|
3
|
+
* exists (confirmed via real research: Cognee's primary SDK is Python; the REST API is the actual
|
|
4
|
+
* integration surface for this ecosystem), so this follows the same pattern already proven in
|
|
5
|
+
* `@wappy_ai/connector-google`'s `oauth.ts` (a plain `fetch`-based client, injectable `fetchImpl` for
|
|
6
|
+
* testing, no SDK dependency).
|
|
7
|
+
*
|
|
8
|
+
* Every request/response shape here was confirmed against a REAL, locally-booted Cognee server
|
|
9
|
+
* (v1.6.2) — its own live OpenAPI spec (`GET /openapi.json`) plus real `curl` round-trips (register →
|
|
10
|
+
* login → create an API key → add → cognify → search → delete), not just read from docs. Concretely
|
|
11
|
+
* confirmed, not assumed:
|
|
12
|
+
* - `POST /api/v1/add` is `multipart/form-data`, fields `raw_data` (string) + `datasetName` — a JSON
|
|
13
|
+
* body is rejected.
|
|
14
|
+
* - `POST /api/v1/cognify` is JSON `{ datasets: [name] }`.
|
|
15
|
+
* - `POST /api/v1/search` is JSON `{ query, searchType, topK? }`. `searchType` matters a lot —
|
|
16
|
+
* the default (`HYBRID_COMPLETION`) returns an LLM-composed answer, not scored chunks; `"CHUNKS"`
|
|
17
|
+
* is what returns a flat array of `{ id, text, score, document_id, document_name, ... }` objects,
|
|
18
|
+
* which is the shape this client maps onto `Knowledge`'s `RecalledChunk`.
|
|
19
|
+
* - Self-hosted auth (when enabled) uses `X-Api-Key: <key>`, NOT `Authorization: Bearer`.
|
|
20
|
+
* - `DELETE /api/v1/datasets` (no path segment) deletes **every** dataset the caller can see,
|
|
21
|
+
* confirmed by a real call — there is no "delete by name in the body" variant. Deleting one
|
|
22
|
+
* dataset requires looking its id up by name via `GET /api/v1/datasets` first, then
|
|
23
|
+
* `DELETE /api/v1/datasets/{id}`.
|
|
24
|
+
*/
|
|
25
|
+
function jsonHeaders(apiKey) {
|
|
26
|
+
return {
|
|
27
|
+
"content-type": "application/json",
|
|
28
|
+
...(apiKey ? { "x-api-key": apiKey } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function authHeaders(apiKey) {
|
|
32
|
+
return apiKey ? { "x-api-key": apiKey } : {};
|
|
33
|
+
}
|
|
34
|
+
async function parseJsonOrThrow(res, action) {
|
|
35
|
+
const text = await res.text();
|
|
36
|
+
let json;
|
|
37
|
+
try {
|
|
38
|
+
json = text ? JSON.parse(text) : {};
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new Error(`Cognee API (${action}): non-JSON response (status ${res.status}): ${text.slice(0, 200)}`);
|
|
42
|
+
}
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
const message = typeof json === "object" && json && "detail" in json ? String(json.detail) : res.statusText;
|
|
45
|
+
throw new Error(`Cognee API (${action}): ${message} (status ${res.status})`);
|
|
46
|
+
}
|
|
47
|
+
return json;
|
|
48
|
+
}
|
|
49
|
+
export function createCogneeClient(opts) {
|
|
50
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
51
|
+
const base = opts.baseUrl.replace(/\/$/, "");
|
|
52
|
+
async function findDatasetId(name) {
|
|
53
|
+
const res = await fetchImpl(`${base}/api/v1/datasets`, {
|
|
54
|
+
method: "GET",
|
|
55
|
+
headers: authHeaders(opts.apiKey),
|
|
56
|
+
});
|
|
57
|
+
const json = await parseJsonOrThrow(res, "findDatasetId");
|
|
58
|
+
const datasets = Array.isArray(json) ? json : [];
|
|
59
|
+
return datasets.find((d) => d.name === name)?.id ?? null;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
async add(datasetName, text) {
|
|
63
|
+
const form = new FormData();
|
|
64
|
+
form.append("raw_data", text);
|
|
65
|
+
form.append("datasetName", datasetName);
|
|
66
|
+
const res = await fetchImpl(`${base}/api/v1/add`, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: authHeaders(opts.apiKey),
|
|
69
|
+
body: form,
|
|
70
|
+
});
|
|
71
|
+
await parseJsonOrThrow(res, "add");
|
|
72
|
+
},
|
|
73
|
+
async cognify(datasetName) {
|
|
74
|
+
const res = await fetchImpl(`${base}/api/v1/cognify`, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: jsonHeaders(opts.apiKey),
|
|
77
|
+
body: JSON.stringify({ datasets: [datasetName], runInBackground: false }),
|
|
78
|
+
});
|
|
79
|
+
await parseJsonOrThrow(res, "cognify");
|
|
80
|
+
},
|
|
81
|
+
async search(query, topK) {
|
|
82
|
+
const res = await fetchImpl(`${base}/api/v1/search`, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: jsonHeaders(opts.apiKey),
|
|
85
|
+
body: JSON.stringify({ query, searchType: "CHUNKS", ...(topK !== undefined ? { topK } : {}) }),
|
|
86
|
+
});
|
|
87
|
+
const json = await parseJsonOrThrow(res, "search");
|
|
88
|
+
// Confirmed live, against TWO real server configurations that return genuinely different
|
|
89
|
+
// shapes for the exact same CHUNKS search: a single-user/no-auth instance
|
|
90
|
+
// (ENABLE_BACKEND_ACCESS_CONTROL=false) returns a bare array of chunk objects directly; the
|
|
91
|
+
// default auth-enabled/multi-tenant instance wraps results per dataset instead —
|
|
92
|
+
// `[{ dataset_id, dataset_name, search_result: [...chunks] }]` — and the default mode is the
|
|
93
|
+
// one anyone self-hosting without extra config will actually hit. Missing this wrapper meant
|
|
94
|
+
// every chunk silently vanished (fell through to the "unanticipated shape" fallback) on a real
|
|
95
|
+
// auth-enabled server, confirmed by comparing the server's own log ("Found 1 chunks from vector
|
|
96
|
+
// search") against this client returning `[]`. Both shapes are handled here; an empty/no-data
|
|
97
|
+
// dataset throws a 4xx with a `detail` string instead (handled by parseJsonOrThrow's !res.ok
|
|
98
|
+
// path above).
|
|
99
|
+
if (!Array.isArray(json))
|
|
100
|
+
return [];
|
|
101
|
+
if (json.length > 0 && typeof json[0] === "object" && json[0] !== null && "search_result" in json[0]) {
|
|
102
|
+
return json.flatMap((envelope) => (Array.isArray(envelope.search_result) ? envelope.search_result : []));
|
|
103
|
+
}
|
|
104
|
+
return json;
|
|
105
|
+
},
|
|
106
|
+
async removeDataset(datasetName) {
|
|
107
|
+
const id = await findDatasetId(datasetName);
|
|
108
|
+
if (!id)
|
|
109
|
+
return; // nothing to remove — never falls back to delete-everything
|
|
110
|
+
const res = await fetchImpl(`${base}/api/v1/datasets/${id}`, {
|
|
111
|
+
method: "DELETE",
|
|
112
|
+
headers: authHeaders(opts.apiKey),
|
|
113
|
+
});
|
|
114
|
+
await parseJsonOrThrow(res, "removeDataset");
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Knowledge } from "@wappy_ai/harness";
|
|
2
|
+
import { type CogneeClientOptions } from "./client.js";
|
|
3
|
+
/**
|
|
4
|
+
* A real `Knowledge` implementation backed by Cognee instead of `@wappy_ai/harness`'s own local
|
|
5
|
+
* (LibSQL/BM25) one — same interface, different backend, following the exact "pluggable, no change
|
|
6
|
+
* to the consuming code" pattern `Knowledge` was already designed for. Cognee's real model (a
|
|
7
|
+
* self-hosted or cloud knowledge-graph service — extracts entities/relationships, described as
|
|
8
|
+
* self-correcting over repeated ingests) is a natural fit for this interface's `ingest`/`recall`
|
|
9
|
+
* shape, unlike `@wappy_ai/core`'s `Memory` (raw per-contact turn history, which this package
|
|
10
|
+
* deliberately does NOT touch — see the plan this was built from).
|
|
11
|
+
*
|
|
12
|
+
* The response mapping below was confirmed against a real, locally-booted Cognee server (ingest a
|
|
13
|
+
* sentence, cognify it, search for it, read back the exact `{id, text, score, document_id,
|
|
14
|
+
* document_name}` shape) — see client.ts's own module doc comment for the full confirmed contract.
|
|
15
|
+
*/
|
|
16
|
+
export type CogneeKnowledgeOptions = CogneeClientOptions;
|
|
17
|
+
export declare function createCogneeKnowledge(opts: CogneeKnowledgeOptions): Knowledge;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createCogneeClient } from "./client.js";
|
|
2
|
+
function toRecalledChunk(index, chunk) {
|
|
3
|
+
// Confirmed live: a CHUNKS search result carries `document_id`/`document_name`, not the dataset
|
|
4
|
+
// name it was ingested under — there is no field in the real response that round-trips the
|
|
5
|
+
// `sourceId` passed to `ingest()`. `document_id` is the closest stable identifier Cognee actually
|
|
6
|
+
// returns, so it's used here; it won't equal the original `sourceId`, by design, not by bug.
|
|
7
|
+
const sourceId = typeof chunk.document_id === "string" ? chunk.document_id : "cognee";
|
|
8
|
+
// Confirmed live, and the opposite of what the field name suggests: Cognee's CHUNKS `score` is a
|
|
9
|
+
// vector DISTANCE (lower = closer match) — verified by ingesting 3 distinct facts and querying
|
|
10
|
+
// each; in every case the obviously-correct chunk came back with the LOWEST score, not the
|
|
11
|
+
// highest. `@wappy_ai/harness`'s own local vector path already normalizes this exact way
|
|
12
|
+
// (`score: 1 - r.dist`, see its knowledge.ts) — mirrored here so `scoreFloor`/sorting behave the
|
|
13
|
+
// same (higher = better) regardless of which Knowledge backend is plugged in.
|
|
14
|
+
const score = typeof chunk.score === "number" ? 1 - chunk.score : 0;
|
|
15
|
+
return {
|
|
16
|
+
id: typeof chunk.id === "string" ? chunk.id : `${sourceId}:${index}`,
|
|
17
|
+
sourceId,
|
|
18
|
+
text: typeof chunk.text === "string" ? chunk.text : JSON.stringify(chunk),
|
|
19
|
+
score,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function createCogneeKnowledge(opts) {
|
|
23
|
+
const client = createCogneeClient(opts);
|
|
24
|
+
return {
|
|
25
|
+
// `chunkOptions` (maxChunkChars/overlapChars) has no server-side analog here — Cognee does its
|
|
26
|
+
// own chunking/extraction as part of `cognify()`, so a caller-specified chunk size can't be
|
|
27
|
+
// honored. Deliberately ignored, not silently misapplied, and documented here rather than
|
|
28
|
+
// pretending to support it.
|
|
29
|
+
async ingest(sourceId, text, chunkOptions) {
|
|
30
|
+
void chunkOptions; // intentionally unused — see the comment above
|
|
31
|
+
if (!text.trim())
|
|
32
|
+
return 0;
|
|
33
|
+
// `ingest()` REPLACES a source's prior content, it does not accrete alongside it — the same
|
|
34
|
+
// contract `@wappy_ai/harness`'s own local Knowledge enforces (it deletes a sourceId's old
|
|
35
|
+
// chunks before inserting new ones). Confirmed live that Cognee itself does NOT do this on its
|
|
36
|
+
// own: calling add() twice under the same dataset name creates two separate documents, and a
|
|
37
|
+
// CHUNKS search then returns both the old and new text side by side — e.g. a stale "budget
|
|
38
|
+
// $2000" chunk kept surfacing alongside a newer "budget $2800" one. So this client removes any
|
|
39
|
+
// existing dataset for `sourceId` first; `removeDataset` is already a no-op when none exists.
|
|
40
|
+
await client.removeDataset(sourceId);
|
|
41
|
+
await client.add(sourceId, text);
|
|
42
|
+
await client.cognify(sourceId);
|
|
43
|
+
// Cognee's cognify response doesn't give a confirmed "chunk count" equivalent (see client.ts's
|
|
44
|
+
// own honest-unknown note) — `1` honestly means "one source successfully ingested," not a
|
|
45
|
+
// literal chunk count the way the local BM25 implementation's return value is.
|
|
46
|
+
return 1;
|
|
47
|
+
},
|
|
48
|
+
async remove(sourceId) {
|
|
49
|
+
await client.removeDataset(sourceId);
|
|
50
|
+
},
|
|
51
|
+
async recall(query, opts) {
|
|
52
|
+
const results = await client.search(query, opts?.topK);
|
|
53
|
+
const scoreFloor = opts?.scoreFloor ?? 0;
|
|
54
|
+
return results
|
|
55
|
+
.map((r, i) => toRecalledChunk(i, r))
|
|
56
|
+
.filter((chunk) => chunk.score > scoreFloor)
|
|
57
|
+
.sort((a, b) => b.score - a.score);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wappy_ai/connector-cognee",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Bring-your-own Cognee (self-hosted or cloud): a real Knowledge/RAG backend for @wappy_ai/harness, backed by Cognee's knowledge-graph API instead of local BM25.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"whatsapp",
|
|
7
|
+
"ai-agent",
|
|
8
|
+
"productivity",
|
|
9
|
+
"cognee",
|
|
10
|
+
"knowledge-graph",
|
|
11
|
+
"rag",
|
|
12
|
+
"typescript"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/csr1010/wappy-kit#readme",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/csr1010/wappy-kit.git",
|
|
18
|
+
"directory": "packages/connector-cognee"
|
|
19
|
+
},
|
|
20
|
+
"bugs": "https://github.com/csr1010/wappy-kit/issues",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"type": "module",
|
|
29
|
+
"main": "dist/index.js",
|
|
30
|
+
"types": "dist/index.d.ts",
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^26.6.2",
|
|
36
|
+
"typescript": "^6.0.3",
|
|
37
|
+
"vitest": "^5.0.1"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@wappy_ai/harness": "0.1.1"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -p tsconfig.json",
|
|
44
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
45
|
+
"test": "vitest run --passWithNoTests"
|
|
46
|
+
}
|
|
47
|
+
}
|