add updates
This commit is contained in:
1 parent
c1aabbb7ee
commit
a57cc024c6
16 files changed
+636
-334
No files matched your search
@@ -35,7 +35,7 @@ export type ResolvedMemorySearchConfig = {
|
||||
baseUrl?: string;
|
||||
apiKey?: string;
|
||||
datasetName?: string;
|
||||
searchType?: "insights" | "chunks" | "summaries";
|
||||
searchType?: "GRAPH_COMPLETION" | "chunks" | "summaries";
|
||||
maxResults?: number;
|
||||
timeoutSeconds?: number;
|
||||
autoCognify?: boolean;
|
||||
|
||||
@@ -501,7 +501,8 @@ const FIELD_HELP: Record<string, string> = {
|
||||
'Sources to index for memory search (default: ["memory"]; add "sessions" to include session transcripts).',
|
||||
"agents.defaults.memorySearch.experimental.sessionMemory":
|
||||
"Enable experimental session transcript indexing for memory search (default: false).",
|
||||
"agents.defaults.memorySearch.provider": 'Embedding provider ("openai", "gemini", or "local").',
|
||||
"agents.defaults.memorySearch.provider":
|
||||
'Embedding provider ("openai", "gemini", "local", or "cognee").',
|
||||
"agents.defaults.memorySearch.remote.baseUrl":
|
||||
"Custom base URL for remote embeddings (OpenAI-compatible proxies or Gemini overrides).",
|
||||
"agents.defaults.memorySearch.remote.apiKey": "Custom API key for the remote embedding provider.",
|
||||
@@ -517,10 +518,12 @@ const FIELD_HELP: Record<string, string> = {
|
||||
"Polling interval in ms for batch status (default: 2000).",
|
||||
"agents.defaults.memorySearch.remote.batch.timeoutMinutes":
|
||||
"Timeout in minutes for batch indexing (default: 60).",
|
||||
"agents.defaults.memorySearch.cognee":
|
||||
"Cognee provider configuration (baseUrl, apiKey, datasetName, searchType, maxResults, autoCognify).",
|
||||
"agents.defaults.memorySearch.local.modelPath":
|
||||
"Local GGUF model path or hf: URI (node-llama-cpp).",
|
||||
"agents.defaults.memorySearch.fallback":
|
||||
'Fallback provider when embeddings fail ("openai", "gemini", "local", or "none").',
|
||||
'Fallback provider when embeddings fail ("openai", "gemini", "local", "cognee", or "none").',
|
||||
"agents.defaults.memorySearch.store.path":
|
||||
"SQLite index path (default: ~/.clawdbot/memory/{agentId}.sqlite).",
|
||||
"agents.defaults.memorySearch.store.vector.enabled":
|
||||
|
||||
@@ -269,8 +269,8 @@ export type MemorySearchConfig = {
|
||||
apiKey?: string;
|
||||
/** Dataset name for organizing memories (default: "clawdbot"). */
|
||||
datasetName?: string;
|
||||
/** Search type: "insights", "chunks", or "summaries" (default: "insights"). */
|
||||
searchType?: "insights" | "chunks" | "summaries";
|
||||
/** Search type: "GRAPH_COMPLETION", "chunks", or "summaries" (default: "GRAPH_COMPLETION"). */
|
||||
searchType?: "GRAPH_COMPLETION" | "chunks" | "summaries";
|
||||
/** Max results per search query (default: 6). */
|
||||
maxResults?: number;
|
||||
/** Timeout for API requests in seconds (default: 30). */
|
||||
|
||||
@@ -310,7 +310,9 @@ export const MemorySearchSchema = z
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
provider: z.union([z.literal("openai"), z.literal("local"), z.literal("gemini")]).optional(),
|
||||
provider: z
|
||||
.union([z.literal("openai"), z.literal("local"), z.literal("gemini"), z.literal("cognee")])
|
||||
.optional(),
|
||||
remote: z
|
||||
.object({
|
||||
baseUrl: z.string().optional(),
|
||||
@@ -330,9 +332,30 @@ export const MemorySearchSchema = z
|
||||
.strict()
|
||||
.optional(),
|
||||
fallback: z
|
||||
.union([z.literal("openai"), z.literal("gemini"), z.literal("local"), z.literal("none")])
|
||||
.union([
|
||||
z.literal("openai"),
|
||||
z.literal("gemini"),
|
||||
z.literal("local"),
|
||||
z.literal("cognee"),
|
||||
z.literal("none"),
|
||||
])
|
||||
.optional(),
|
||||
model: z.string().optional(),
|
||||
cognee: z
|
||||
.object({
|
||||
baseUrl: z.string().optional(),
|
||||
apiKey: z.string().optional(),
|
||||
datasetName: z.string().optional(),
|
||||
searchType: z
|
||||
.union([z.literal("GRAPH_COMPLETION"), z.literal("CHUNKS"), z.literal("SUMMARIES")])
|
||||
.optional(),
|
||||
maxResults: z.number().int().positive().optional(),
|
||||
autoCognify: z.boolean().optional(),
|
||||
cognifyBatchSize: z.number().int().positive().optional(),
|
||||
timeoutSeconds: z.number().int().positive().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
local: z
|
||||
.object({
|
||||
modelPath: z.string().optional(),
|
||||
|
||||
@@ -2,21 +2,21 @@ import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { CogneeClient } from "./cognee-client.js";
|
||||
import { request } from "undici";
|
||||
|
||||
vi.mock("undici", () => ({
|
||||
vi.mock("undici", () => ({
|
||||
request: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("CogneeClient", () => {
|
||||
beforeEach(() => {
|
||||
describe("CogneeClient", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("add", () => {
|
||||
it("should add data successfully", async () => {
|
||||
describe("add", () => {
|
||||
it("should add data successfully", async () => {
|
||||
const mockResponse = {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
@@ -46,18 +46,18 @@ describe("CogneeClient", () => {
|
||||
message: "Data added successfully",
|
||||
});
|
||||
expect(request).toHaveBeenCalledWith(
|
||||
"http://localhost:8000/add",
|
||||
"http://localhost:8000/api/v1/add",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: expect.objectContaining({
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer test-key",
|
||||
"X-Api-Key": "test-key",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle errors", async () => {
|
||||
it("should handle errors", async () => {
|
||||
const mockResponse = {
|
||||
statusCode: 500,
|
||||
body: {
|
||||
@@ -77,8 +77,8 @@ describe("CogneeClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("cognify", () => {
|
||||
it("should run cognify successfully", async () => {
|
||||
describe("cognify", () => {
|
||||
it("should run cognify successfully", async () => {
|
||||
const mockResponse = {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
@@ -106,8 +106,8 @@ describe("CogneeClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("search", () => {
|
||||
it("should search successfully", async () => {
|
||||
describe("search", () => {
|
||||
it("should search successfully", async () => {
|
||||
const mockResponse = {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
@@ -121,7 +121,7 @@ describe("CogneeClient", () => {
|
||||
},
|
||||
],
|
||||
query: "test query",
|
||||
search_type: "insights",
|
||||
search_type: "GRAPH_COMPLETION",
|
||||
}),
|
||||
text: vi.fn(),
|
||||
},
|
||||
@@ -132,7 +132,7 @@ describe("CogneeClient", () => {
|
||||
|
||||
const result = await client.search({
|
||||
queryText: "test query",
|
||||
searchType: "insights",
|
||||
searchType: "GRAPH_COMPLETION",
|
||||
});
|
||||
|
||||
expect(result.results).toHaveLength(1);
|
||||
@@ -145,14 +145,14 @@ describe("CogneeClient", () => {
|
||||
expect(result.query).toBe("test query");
|
||||
});
|
||||
|
||||
it("should use default search type", async () => {
|
||||
it("should use default search type", async () => {
|
||||
const mockResponse = {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
json: vi.fn().mockResolvedValue({
|
||||
results: [],
|
||||
query: "test",
|
||||
search_type: "insights",
|
||||
search_type: "GRAPH_COMPLETION",
|
||||
}),
|
||||
text: vi.fn(),
|
||||
},
|
||||
@@ -165,28 +165,18 @@ describe("CogneeClient", () => {
|
||||
expect(request).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
body: expect.stringContaining('"search_type":"insights"'),
|
||||
body: expect.stringContaining('"searchType":"GRAPH_COMPLETION"'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("status", () => {
|
||||
it("should get status successfully", async () => {
|
||||
describe("status", () => {
|
||||
it("should get status successfully", async () => {
|
||||
const mockResponse = {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
json: vi.fn().mockResolvedValue({
|
||||
status: "healthy",
|
||||
version: "1.0.0",
|
||||
datasets: [
|
||||
{
|
||||
id: "dataset-1",
|
||||
name: "test-dataset",
|
||||
document_count: 10,
|
||||
},
|
||||
],
|
||||
}),
|
||||
json: vi.fn().mockResolvedValue({ status: "healthy" }),
|
||||
text: vi.fn(),
|
||||
},
|
||||
};
|
||||
@@ -198,20 +188,12 @@ describe("CogneeClient", () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
status: "healthy",
|
||||
version: "1.0.0",
|
||||
datasets: [
|
||||
{
|
||||
id: "dataset-1",
|
||||
name: "test-dataset",
|
||||
documentCount: 10,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("healthCheck", () => {
|
||||
it("should return true when status is successful", async () => {
|
||||
describe("healthCheck", () => {
|
||||
it("should return true when status is successful", async () => {
|
||||
const mockResponse = {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
@@ -227,7 +209,7 @@ describe("CogneeClient", () => {
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when status fails", async () => {
|
||||
it("should return false when status fails", async () => {
|
||||
vi.mocked(request).mockRejectedValue(new Error("Connection failed"));
|
||||
|
||||
const client = new CogneeClient();
|
||||
|
||||
+104
-75
@@ -1,10 +1,12 @@
|
||||
import { request } from "undici";
|
||||
import { Blob } from "buffer";
|
||||
import { FormData, request } from "undici";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
|
||||
const log = createSubsystemLogger("cognee");
|
||||
|
||||
const DEFAULT_BASE_URL = "http://localhost:8000";
|
||||
const DEFAULT_TIMEOUT_MS = 30_000;
|
||||
const API_PREFIX = "/api/v1";
|
||||
|
||||
export type CogneeClientConfig = {
|
||||
baseUrl?: string;
|
||||
@@ -35,7 +37,7 @@ export type CogneeCognifyResponse = {
|
||||
|
||||
export type CogneeSearchRequest = {
|
||||
queryText: string;
|
||||
searchType?: "insights" | "chunks" | "summaries";
|
||||
searchType?: "GRAPH_COMPLETION" | "chunks" | "summaries";
|
||||
datasetIds?: string[];
|
||||
};
|
||||
|
||||
@@ -43,7 +45,7 @@ export type CogneeSearchResult = {
|
||||
id: string;
|
||||
text: string;
|
||||
score: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type CogneeSearchResponse = {
|
||||
@@ -55,13 +57,15 @@ export type CogneeSearchResponse = {
|
||||
export type CogneeStatusResponse = {
|
||||
status: string;
|
||||
version?: string;
|
||||
datasets?: Array<{
|
||||
datasets?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
documentCount?: number;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
|
||||
type CogneeSearchApiType = "SUMMARIES" | "CHUNKS" | "GRAPH_COMPLETION";
|
||||
|
||||
export class CogneeClient {
|
||||
private readonly baseUrl: string;
|
||||
private readonly apiKey?: string;
|
||||
@@ -73,12 +77,11 @@ export class CogneeClient {
|
||||
this.timeoutMs = config.timeoutMs || DEFAULT_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
async add(req: CogneeAddRequest): Promise<CogneeAddResponse> {
|
||||
const url = `${this.baseUrl}/add`;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
async add(req: CogneeAddRequest): Promise<CogneeAddResponse> {
|
||||
const url = `${this.baseUrl}${API_PREFIX}/add`;
|
||||
const headers: Record<string, string> = {};
|
||||
if (this.apiKey) {
|
||||
headers.Authorization = `Bearer ${this.apiKey}`;
|
||||
headers["X-Api-Key"] = this.apiKey;
|
||||
}
|
||||
|
||||
@@ -89,23 +92,27 @@ export class CogneeClient {
|
||||
});
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
const blob = new Blob([req.data], { type: "text/plain" });
|
||||
formData.append("data", blob, "clawdbot-memory.txt");
|
||||
if (req.datasetName) {
|
||||
formData.append("datasetName", req.datasetName);
|
||||
}
|
||||
if (req.datasetId) {
|
||||
formData.append("datasetId", req.datasetId);
|
||||
}
|
||||
|
||||
const response = await request(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
data: req.data,
|
||||
dataset_name: req.datasetName,
|
||||
dataset_id: req.datasetId,
|
||||
}),
|
||||
body: formData,
|
||||
bodyTimeout: this.timeoutMs,
|
||||
headersTimeout: this.timeoutMs,
|
||||
});
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
const errorText = await response.body.text();
|
||||
throw new Error(
|
||||
`Cognee add failed with status ${response.statusCode}: ${errorText}`,
|
||||
);
|
||||
throw new Error(`Cognee add failed with status ${response.statusCode}: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = (await response.body.json()) as {
|
||||
@@ -127,12 +134,13 @@ export class CogneeClient {
|
||||
}
|
||||
}
|
||||
|
||||
async cognify(req: CogneeCognifyRequest = {}): Promise<CogneeCognifyResponse> {
|
||||
const url = `${this.baseUrl}/cognify`;
|
||||
const headers: Record<string, string> = {
|
||||
async cognify(req: CogneeCognifyRequest = {}): Promise<CogneeCognifyResponse> {
|
||||
const url = `${this.baseUrl}${API_PREFIX}/cognify`;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
if (this.apiKey) {
|
||||
headers.Authorization = `Bearer ${this.apiKey}`;
|
||||
headers["X-Api-Key"] = this.apiKey;
|
||||
}
|
||||
|
||||
@@ -143,7 +151,7 @@ export class CogneeClient {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
dataset_ids: req.datasetIds,
|
||||
datasetIds: req.datasetIds,
|
||||
}),
|
||||
bodyTimeout: this.timeoutMs,
|
||||
headersTimeout: this.timeoutMs,
|
||||
@@ -151,9 +159,7 @@ export class CogneeClient {
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
const errorText = await response.body.text();
|
||||
throw new Error(
|
||||
`Cognee cognify failed with status ${response.statusCode}: ${errorText}`,
|
||||
);
|
||||
throw new Error(`Cognee cognify failed with status ${response.statusCode}: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = (await response.body.json()) as {
|
||||
@@ -173,12 +179,13 @@ export class CogneeClient {
|
||||
}
|
||||
}
|
||||
|
||||
async search(req: CogneeSearchRequest): Promise<CogneeSearchResponse> {
|
||||
const url = `${this.baseUrl}/search`;
|
||||
const headers: Record<string, string> = {
|
||||
async search(req: CogneeSearchRequest): Promise<CogneeSearchResponse> {
|
||||
const url = `${this.baseUrl}${API_PREFIX}/search`;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
if (this.apiKey) {
|
||||
headers.Authorization = `Bearer ${this.apiKey}`;
|
||||
headers["X-Api-Key"] = this.apiKey;
|
||||
}
|
||||
|
||||
@@ -193,9 +200,9 @@ export class CogneeClient {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
query_text: req.queryText,
|
||||
search_type: req.searchType || "insights",
|
||||
dataset_ids: req.datasetIds,
|
||||
query: req.queryText,
|
||||
searchType: this.mapSearchType(req.searchType),
|
||||
datasetIds: req.datasetIds,
|
||||
}),
|
||||
bodyTimeout: this.timeoutMs,
|
||||
headersTimeout: this.timeoutMs,
|
||||
@@ -203,31 +210,16 @@ export class CogneeClient {
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
const errorText = await response.body.text();
|
||||
throw new Error(
|
||||
`Cognee search failed with status ${response.statusCode}: ${errorText}`,
|
||||
);
|
||||
throw new Error(`Cognee search failed with status ${response.statusCode}: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = (await response.body.json()) as {
|
||||
results: Array<{
|
||||
id: string;
|
||||
text: string;
|
||||
score: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
}>;
|
||||
query: string;
|
||||
search_type: string;
|
||||
};
|
||||
const data = (await response.body.json()) as unknown;
|
||||
const results = this.normalizeSearchResults(data);
|
||||
|
||||
return {
|
||||
results: data.results.map((r) => ({
|
||||
id: r.id,
|
||||
text: r.text,
|
||||
score: r.score,
|
||||
metadata: r.metadata,
|
||||
})),
|
||||
query: data.query,
|
||||
searchType: data.search_type,
|
||||
results,
|
||||
query: req.queryText,
|
||||
searchType: req.searchType || "GRAPH_COMPLETION",
|
||||
};
|
||||
} catch (error) {
|
||||
log.error("Failed to search Cognee", { error });
|
||||
@@ -237,10 +229,11 @@ export class CogneeClient {
|
||||
}
|
||||
}
|
||||
|
||||
async status(): Promise<CogneeStatusResponse> {
|
||||
const url = `${this.baseUrl}/status`;
|
||||
const headers: Record<string, string> = {};
|
||||
async status(): Promise<CogneeStatusResponse> {
|
||||
const url = `${this.baseUrl}/health`;
|
||||
const headers: Record<string, string> = {};
|
||||
if (this.apiKey) {
|
||||
headers.Authorization = `Bearer ${this.apiKey}`;
|
||||
headers["X-Api-Key"] = this.apiKey;
|
||||
}
|
||||
|
||||
@@ -256,29 +249,13 @@ export class CogneeClient {
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
const errorText = await response.body.text();
|
||||
throw new Error(
|
||||
`Cognee status failed with status ${response.statusCode}: ${errorText}`,
|
||||
);
|
||||
throw new Error(`Cognee status failed with status ${response.statusCode}: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = (await response.body.json()) as {
|
||||
status: string;
|
||||
version?: string;
|
||||
datasets?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
document_count?: number;
|
||||
}>;
|
||||
};
|
||||
const data = (await response.body.json()) as { status?: string };
|
||||
|
||||
return {
|
||||
status: data.status,
|
||||
version: data.version,
|
||||
datasets: data.datasets?.map((d) => ({
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
documentCount: d.document_count,
|
||||
})),
|
||||
status: data.status || "healthy",
|
||||
};
|
||||
} catch (error) {
|
||||
log.error("Failed to get Cognee status", { error });
|
||||
@@ -288,7 +265,7 @@ export class CogneeClient {
|
||||
}
|
||||
}
|
||||
|
||||
async healthCheck(): Promise<boolean> {
|
||||
async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
await this.status();
|
||||
return true;
|
||||
@@ -296,4 +273,56 @@ export class CogneeClient {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private mapSearchType(type?: CogneeSearchRequest["searchType"]): CogneeSearchApiType {
|
||||
switch (type) {
|
||||
case "chunks":
|
||||
return "CHUNKS";
|
||||
case "summaries":
|
||||
return "SUMMARIES";
|
||||
case "GRAPH_COMPLETION":
|
||||
default:
|
||||
return "GRAPH_COMPLETION";
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeSearchResults(data: unknown): CogneeSearchResult[] {
|
||||
if (Array.isArray(data)) {
|
||||
return data.map((item, index) => {
|
||||
if (typeof item === "string") {
|
||||
return { id: `result-${index}`, text: item, score: 0 };
|
||||
}
|
||||
|
||||
if (item && typeof item === "object") {
|
||||
const record = item as Record<string, unknown>;
|
||||
const raw =
|
||||
record.search_result ?? record.result ?? record.context ?? record.text ?? record;
|
||||
const text = typeof raw === "string" ? raw : JSON.stringify(raw, null, 2);
|
||||
const metadata =
|
||||
record.dataset_name || record.dataset_id
|
||||
? {
|
||||
datasetName: record.dataset_name,
|
||||
datasetId: record.dataset_id,
|
||||
}
|
||||
: undefined;
|
||||
return { id: `result-${index}`, text, score: 0, metadata };
|
||||
}
|
||||
|
||||
return {
|
||||
id: `result-${index}`,
|
||||
text: String(item),
|
||||
score: 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (data && typeof data === "object" && "results" in data) {
|
||||
const results = (data as { results?: unknown }).results;
|
||||
if (Array.isArray(results)) {
|
||||
return this.normalizeSearchResults(results);
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { CogneeMemoryProvider } from "./cognee-provider.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
|
||||
vi.mock("./cognee-client.js", () => ({
|
||||
CogneeClient: vi.fn().mockImplementation(() => ({
|
||||
vi.mock("./cognee-client.js", () => ({
|
||||
CogneeClient: vi.fn().mockImplementation(() => ({
|
||||
healthCheck: vi.fn().mockResolvedValue(true),
|
||||
add: vi.fn().mockResolvedValue({
|
||||
datasetId: "test-dataset-id",
|
||||
@@ -27,7 +27,7 @@ vi.mock("./cognee-client.js", () => ({
|
||||
},
|
||||
],
|
||||
query: "test query",
|
||||
searchType: "insights",
|
||||
searchType: "GRAPH_COMPLETION",
|
||||
}),
|
||||
status: vi.fn().mockResolvedValue({
|
||||
status: "healthy",
|
||||
@@ -43,13 +43,13 @@ vi.mock("./cognee-client.js", () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./internal.js", () => ({
|
||||
vi.mock("./internal.js", () => ({
|
||||
listMemoryFiles: vi.fn().mockResolvedValue([]),
|
||||
buildFileEntry: vi.fn(),
|
||||
hashText: vi.fn().mockReturnValue("test-hash"),
|
||||
}));
|
||||
|
||||
vi.mock("node:fs/promises", () => ({
|
||||
vi.mock("node:fs/promises", () => ({
|
||||
default: {
|
||||
readdir: vi.fn().mockResolvedValue([]),
|
||||
readFile: vi.fn().mockResolvedValue("Test file content"),
|
||||
@@ -57,7 +57,7 @@ vi.mock("node:fs/promises", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
describe("CogneeMemoryProvider", () => {
|
||||
describe("CogneeMemoryProvider", () => {
|
||||
const mockConfig: ClawdbotConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
@@ -66,18 +66,18 @@ describe("CogneeMemoryProvider", () => {
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should initialize with default configuration", () => {
|
||||
describe("constructor", () => {
|
||||
it("should initialize with default configuration", () => {
|
||||
const provider = new CogneeMemoryProvider(mockConfig, "test-agent", ["memory"]);
|
||||
|
||||
expect(provider).toBeDefined();
|
||||
});
|
||||
|
||||
it("should initialize with custom configuration", () => {
|
||||
it("should initialize with custom configuration", () => {
|
||||
const provider = new CogneeMemoryProvider(mockConfig, "test-agent", ["memory"], {
|
||||
baseUrl: "http://custom:8000",
|
||||
apiKey: "custom-key",
|
||||
@@ -90,8 +90,8 @@ describe("CogneeMemoryProvider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("healthCheck", () => {
|
||||
it("should perform health check", async () => {
|
||||
describe("healthCheck", () => {
|
||||
it("should perform health check", async () => {
|
||||
const provider = new CogneeMemoryProvider(mockConfig, "test-agent", ["memory"]);
|
||||
|
||||
const healthy = await provider.healthCheck();
|
||||
@@ -100,8 +100,8 @@ describe("CogneeMemoryProvider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("search", () => {
|
||||
it("should search and transform results", async () => {
|
||||
describe("search", () => {
|
||||
it("should search and transform results", async () => {
|
||||
const provider = new CogneeMemoryProvider(mockConfig, "test-agent", ["memory"]);
|
||||
|
||||
const results = await provider.search("test query");
|
||||
@@ -115,13 +115,10 @@ describe("CogneeMemoryProvider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should respect maxResults setting", async () => {
|
||||
const provider = new CogneeMemoryProvider(
|
||||
mockConfig,
|
||||
"test-agent",
|
||||
["memory"],
|
||||
{ maxResults: 5 },
|
||||
);
|
||||
it("should respect maxResults setting", async () => {
|
||||
const provider = new CogneeMemoryProvider(mockConfig, "test-agent", ["memory"], {
|
||||
maxResults: 5,
|
||||
});
|
||||
|
||||
const results = await provider.search("test query");
|
||||
|
||||
@@ -129,16 +126,16 @@ describe("CogneeMemoryProvider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("cognify", () => {
|
||||
it("should run cognify", async () => {
|
||||
describe("cognify", () => {
|
||||
it("should run cognify", async () => {
|
||||
const provider = new CogneeMemoryProvider(mockConfig, "test-agent", ["memory"]);
|
||||
|
||||
await expect(provider.cognify()).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStatus", () => {
|
||||
it("should return status information", async () => {
|
||||
describe("getStatus", () => {
|
||||
it("should return status information", async () => {
|
||||
const provider = new CogneeMemoryProvider(mockConfig, "test-agent", ["memory"]);
|
||||
|
||||
const status = await provider.getStatus();
|
||||
|
||||
+137
-36
@@ -5,22 +5,20 @@ import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
||||
import { resolveSessionTranscriptsDirForAgent } from "../config/sessions/paths.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import type { MemorySearchResult } from "./index.js";
|
||||
import {
|
||||
CogneeClient,
|
||||
type CogneeClientConfig,
|
||||
type CogneeSearchResult,
|
||||
} from "./cognee-client.js";
|
||||
import { CogneeClient, type CogneeClientConfig, type CogneeSearchResult } from "./cognee-client.js";
|
||||
import {
|
||||
buildFileEntry,
|
||||
hashText,
|
||||
listMemoryFiles,
|
||||
isMemoryPath,
|
||||
normalizeRelPath,
|
||||
type MemoryFileEntry,
|
||||
} from "./internal.js";
|
||||
|
||||
const log = createSubsystemLogger("cognee-provider");
|
||||
|
||||
const DEFAULT_DATASET_NAME = "clawdbot";
|
||||
const DEFAULT_SEARCH_TYPE = "insights";
|
||||
const DEFAULT_SEARCH_TYPE = "GRAPH_COMPLETION";
|
||||
const DEFAULT_MAX_RESULTS = 6;
|
||||
const DEFAULT_TIMEOUT_SECONDS = 30;
|
||||
const DEFAULT_AUTO_COGNIFY = true;
|
||||
@@ -31,7 +29,7 @@ export type CogneeProviderConfig = {
|
||||
baseUrl?: string;
|
||||
apiKey?: string;
|
||||
datasetName?: string;
|
||||
searchType?: "insights" | "chunks" | "summaries";
|
||||
searchType?: "GRAPH_COMPLETION" | "chunks" | "summaries";
|
||||
maxResults?: number;
|
||||
timeoutSeconds?: number;
|
||||
autoCognify?: boolean;
|
||||
@@ -46,18 +44,18 @@ export class CogneeMemoryProvider {
|
||||
private readonly agentId: string;
|
||||
private readonly workspaceDir: string;
|
||||
private readonly datasetName: string;
|
||||
private readonly searchType: "insights" | "chunks" | "summaries";
|
||||
private readonly searchType: "GRAPH_COMPLETION" | "chunks" | "summaries";
|
||||
private readonly maxResults: number;
|
||||
private readonly autoCognify: boolean;
|
||||
private readonly cognifyBatchSize: number;
|
||||
private readonly sources: Set<CogneeMemorySource>;
|
||||
private readonly sources: Set<CogneeMemorySource>;
|
||||
private datasetId?: string;
|
||||
private syncedFiles = new Map<string, string>(); // path -> hash
|
||||
private syncedFiles = new Map<string, string>(); // path -> hash
|
||||
|
||||
constructor(
|
||||
cfg: ClawdbotConfig,
|
||||
agentId: string,
|
||||
sources: Array<CogneeMemorySource>,
|
||||
sources: Array<CogneeMemorySource>,
|
||||
config: CogneeProviderConfig = {},
|
||||
) {
|
||||
const timeoutMs = (config.timeoutSeconds || DEFAULT_TIMEOUT_SECONDS) * 1000;
|
||||
@@ -86,11 +84,15 @@ export class CogneeMemoryProvider {
|
||||
});
|
||||
}
|
||||
|
||||
async healthCheck(): Promise<boolean> {
|
||||
async healthCheck(): Promise<boolean> {
|
||||
return await this.client.healthCheck();
|
||||
}
|
||||
|
||||
async sync(): Promise<void> {
|
||||
async sync(params?: {
|
||||
reason?: string;
|
||||
force?: boolean;
|
||||
progress?: (update: { completed: number; total: number; label?: string }) => void;
|
||||
}): Promise<void> {
|
||||
log.info("Starting Cognee memory sync", { agentId: this.agentId });
|
||||
|
||||
let addedCount = 0;
|
||||
@@ -108,7 +110,7 @@ export class CogneeMemoryProvider {
|
||||
}
|
||||
|
||||
// Run cognify if auto-enabled and files were added
|
||||
if (this.autoCognify && addedCount > 0) {
|
||||
if (this.autoCognify && addedCount > 0) {
|
||||
log.info("Running cognify after sync", { addedCount });
|
||||
await this.cognify();
|
||||
}
|
||||
@@ -117,9 +119,20 @@ export class CogneeMemoryProvider {
|
||||
agentId: this.agentId,
|
||||
addedCount,
|
||||
});
|
||||
|
||||
if (params?.progress) {
|
||||
params.progress({
|
||||
completed: addedCount,
|
||||
total: addedCount,
|
||||
label: params.reason ? `Synced (${params.reason})` : "Synced",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async search(query: string): Promise<MemorySearchResult[]> {
|
||||
async search(
|
||||
query: string,
|
||||
opts?: { maxResults?: number; minScore?: number; sessionKey?: string },
|
||||
): Promise<MemorySearchResult[]> {
|
||||
log.debug("Searching Cognee memory", { query, searchType: this.searchType });
|
||||
|
||||
try {
|
||||
@@ -129,9 +142,12 @@ export class CogneeMemoryProvider {
|
||||
datasetIds: this.datasetId ? [this.datasetId] : undefined,
|
||||
});
|
||||
|
||||
const maxResults = opts?.maxResults ?? this.maxResults;
|
||||
const minScore = opts?.minScore ?? 0;
|
||||
const results: MemorySearchResult[] = response.results
|
||||
.slice(0, this.maxResults)
|
||||
.map((r) => this.transformResult(r));
|
||||
.map((r) => this.transformResult(r))
|
||||
.filter((r) => r.score >= minScore)
|
||||
.slice(0, maxResults);
|
||||
|
||||
log.debug("Cognee search completed", { query, resultCount: results.length });
|
||||
return results;
|
||||
@@ -141,7 +157,7 @@ export class CogneeMemoryProvider {
|
||||
}
|
||||
}
|
||||
|
||||
async cognify(): Promise<void> {
|
||||
async cognify(): Promise<void> {
|
||||
try {
|
||||
const response = await this.client.cognify({
|
||||
datasetIds: this.datasetId ? [this.datasetId] : undefined,
|
||||
@@ -153,16 +169,16 @@ export class CogneeMemoryProvider {
|
||||
}
|
||||
}
|
||||
|
||||
async getStatus(): Promise<{
|
||||
async getStatus(): Promise<{
|
||||
connected: boolean;
|
||||
datasetId?: string;
|
||||
datasetName: string;
|
||||
syncedFileCount: number;
|
||||
version?: string;
|
||||
}> {
|
||||
}> {
|
||||
try {
|
||||
const status = await this.client.status();
|
||||
const dataset = status.datasets?.find((d) => d.name === this.datasetName);
|
||||
const dataset = status.datasets?.find((d) => d.name === this.datasetName);
|
||||
|
||||
return {
|
||||
connected: true,
|
||||
@@ -181,7 +197,98 @@ export class CogneeMemoryProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private async collectMemoryFiles(): Promise<MemoryFileEntry[]> {
|
||||
status(): {
|
||||
files: number;
|
||||
chunks: number;
|
||||
dirty: boolean;
|
||||
workspaceDir: string;
|
||||
dbPath: string;
|
||||
provider: string;
|
||||
model: string;
|
||||
requestedProvider: string;
|
||||
sources: Array<CogneeMemorySource>;
|
||||
sourceCounts: Array<{ source: CogneeMemorySource; files: number; chunks: number }>;
|
||||
cache?: { enabled: boolean; entries?: number; maxEntries?: number };
|
||||
fts?: { enabled: boolean; available: boolean; error?: string };
|
||||
fallback?: { from: string; reason?: string };
|
||||
vector?: {
|
||||
enabled: boolean;
|
||||
available?: boolean;
|
||||
extensionPath?: string;
|
||||
loadError?: string;
|
||||
dims?: number;
|
||||
};
|
||||
batch?: {
|
||||
enabled: boolean;
|
||||
failures: number;
|
||||
limit: number;
|
||||
wait: boolean;
|
||||
concurrency: number;
|
||||
pollIntervalMs: number;
|
||||
timeoutMs: number;
|
||||
lastError?: string;
|
||||
lastProvider?: string;
|
||||
};
|
||||
} {
|
||||
const sources = Array.from(this.sources);
|
||||
const files = this.syncedFiles.size;
|
||||
return {
|
||||
files,
|
||||
chunks: 0,
|
||||
dirty: false,
|
||||
workspaceDir: this.workspaceDir,
|
||||
dbPath: "cognee",
|
||||
provider: "cognee",
|
||||
model: this.searchType,
|
||||
requestedProvider: "cognee",
|
||||
sources,
|
||||
sourceCounts: sources.map((source) => ({ source, files, chunks: 0 })),
|
||||
vector: {
|
||||
enabled: false,
|
||||
available: false,
|
||||
},
|
||||
fts: {
|
||||
enabled: false,
|
||||
available: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async readFile(params: {
|
||||
relPath: string;
|
||||
from?: number;
|
||||
lines?: number;
|
||||
}): Promise<{ text: string; path: string }> {
|
||||
const relPath = normalizeRelPath(params.relPath);
|
||||
if (!relPath || !isMemoryPath(relPath)) {
|
||||
throw new Error("path required");
|
||||
}
|
||||
const absPath = path.resolve(this.workspaceDir, relPath);
|
||||
if (!absPath.startsWith(this.workspaceDir)) {
|
||||
throw new Error("path escapes workspace");
|
||||
}
|
||||
const content = await fs.readFile(absPath, "utf-8");
|
||||
if (!params.from && !params.lines) {
|
||||
return { text: content, path: relPath };
|
||||
}
|
||||
const lines = content.split("\n");
|
||||
const start = Math.max(1, params.from ?? 1);
|
||||
const count = Math.max(1, params.lines ?? lines.length);
|
||||
const slice = lines.slice(start - 1, start - 1 + count);
|
||||
return { text: slice.join("\n"), path: relPath };
|
||||
}
|
||||
|
||||
async probeEmbeddingAvailability(): Promise<{ ok: boolean; error?: string }> {
|
||||
return { ok: false, error: "Cognee provider does not use embeddings." };
|
||||
}
|
||||
|
||||
async probeVectorAvailability(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async close(): Promise<void> {}
|
||||
|
||||
private async collectMemoryFiles(): Promise<MemoryFileEntry[]> {
|
||||
const files: MemoryFileEntry[] = [];
|
||||
const memoryPaths = await listMemoryFiles(this.workspaceDir);
|
||||
|
||||
@@ -197,12 +304,9 @@ export class CogneeMemoryProvider {
|
||||
return files;
|
||||
}
|
||||
|
||||
private async collectSessionFiles(): Promise<MemoryFileEntry[]> {
|
||||
private async collectSessionFiles(): Promise<MemoryFileEntry[]> {
|
||||
const files: MemoryFileEntry[] = [];
|
||||
const transcriptsDir = resolveSessionTranscriptsDirForAgent(
|
||||
this.cfg,
|
||||
this.agentId,
|
||||
);
|
||||
const transcriptsDir = resolveSessionTranscriptsDirForAgent(this.agentId);
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(transcriptsDir, { withFileTypes: true });
|
||||
@@ -227,20 +331,17 @@ export class CogneeMemoryProvider {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log.debug("No session transcripts directory", { transcriptsDir });
|
||||
log.debug("No session transcripts directory", { transcriptsDir, error });
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
private async syncFiles(
|
||||
files: MemoryFileEntry[],
|
||||
source: CogneeMemorySource,
|
||||
): Promise<number> {
|
||||
private async syncFiles(files: MemoryFileEntry[], source: CogneeMemorySource): Promise<number> {
|
||||
let addedCount = 0;
|
||||
const batchSize = this.cognifyBatchSize;
|
||||
|
||||
for (let i = 0; i < files.length; i += batchSize) {
|
||||
for (let i = 0; i < files.length; i += batchSize) {
|
||||
const batch = files.slice(i, i + batchSize);
|
||||
|
||||
for (const file of batch) {
|
||||
@@ -295,7 +396,7 @@ export class CogneeMemoryProvider {
|
||||
|
||||
// Truncate snippet to max chars
|
||||
let snippet = result.text;
|
||||
if (snippet.length > SNIPPET_MAX_CHARS) {
|
||||
if (snippet.length > SNIPPET_MAX_CHARS) {
|
||||
snippet = snippet.slice(0, SNIPPET_MAX_CHARS) + "...";
|
||||
}
|
||||
|
||||
@@ -313,9 +414,9 @@ export class CogneeMemoryProvider {
|
||||
export async function createCogneeProvider(
|
||||
cfg: ClawdbotConfig,
|
||||
agentId: string,
|
||||
sources: Array<CogneeMemorySource>,
|
||||
sources: Array<CogneeMemorySource>,
|
||||
config: CogneeProviderConfig = {},
|
||||
): Promise<CogneeMemoryProvider> {
|
||||
): Promise<CogneeMemoryProvider> {
|
||||
const provider = new CogneeMemoryProvider(cfg, agentId, sources, config);
|
||||
|
||||
// Verify connection
|
||||
|
||||
@@ -183,13 +183,15 @@ export class MemoryIndexManager {
|
||||
const key = `${agentId}:${workspaceDir}:${JSON.stringify(settings)}`;
|
||||
const existing = INDEX_CACHE.get(key);
|
||||
if (existing) return existing;
|
||||
const provider = settings.provider === "cognee" ? "auto" : settings.provider;
|
||||
const fallback = settings.fallback === "cognee" ? "none" : settings.fallback;
|
||||
const providerResult = await createEmbeddingProvider({
|
||||
config: cfg,
|
||||
agentDir: resolveAgentDir(cfg, agentId),
|
||||
provider: settings.provider,
|
||||
provider,
|
||||
remote: settings.remote,
|
||||
model: settings.model,
|
||||
fallback: settings.fallback,
|
||||
fallback,
|
||||
local: settings.local,
|
||||
});
|
||||
const manager = new MemoryIndexManager({
|
||||
@@ -197,7 +199,7 @@ export class MemoryIndexManager {
|
||||
cfg,
|
||||
agentId,
|
||||
workspaceDir,
|
||||
settings,
|
||||
settings: { ...settings, provider, fallback },
|
||||
providerResult,
|
||||
});
|
||||
INDEX_CACHE.set(key, manager);
|
||||
@@ -1261,7 +1263,7 @@ export class MemoryIndexManager {
|
||||
}
|
||||
|
||||
private async activateFallbackProvider(reason: string): Promise<boolean> {
|
||||
const fallback = this.settings.fallback;
|
||||
const fallback = this.settings.fallback === "cognee" ? "none" : this.settings.fallback;
|
||||
if (!fallback || fallback === "none" || fallback === this.provider.id) return false;
|
||||
if (this.fallbackFrom) return false;
|
||||
const fallbackFrom = this.provider.id as "openai" | "gemini" | "local";
|
||||
|
||||
Reference in new issue
Block a user