Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ async function main() {
if (isGraphExtractionEnabled()) {
registerGraphFunction(sdk, kv, provider);
console.log(`[agentmemory] Knowledge graph: extraction enabled`);
} else {
console.log(
`[agentmemory] Knowledge graph: disabled (set GRAPH_EXTRACTION_ENABLED=true in the agentmemory host environment or ~/.agentmemory/.env, then restart)`,
);
}

registerConsolidationPipelineFunction(sdk, kv, provider);
Expand Down
36 changes: 27 additions & 9 deletions src/triggers/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ function graphDisabledResponse(): Response {
});
}

function graphRequestFailedResponse(functionId: string, err: unknown): Response {
const message = err instanceof Error ? err.message : String(err);
return {
status_code: 500,
body: {
error: "Knowledge graph request failed",
functionId,
message,
},
};
}

function consolidationDisabledResponse(): Response {
return flagDisabledResponse({
error: "Consolidation pipeline not enabled",
Expand Down Expand Up @@ -1060,11 +1072,13 @@ export function registerApiTriggers(
): Promise<Response> => {
const authErr = checkAuth(req, secret);
if (authErr) return authErr;
if (!isGraphExtractionEnabled()) return graphDisabledResponse();
const functionId = "mem::graph-query";
try {
const result = await sdk.trigger({ function_id: "mem::graph-query", payload: req.body || {} });
const result = await sdk.trigger({ function_id: functionId, payload: req.body || {} });
return { status_code: 200, body: result };
} catch {
return graphDisabledResponse();
} catch (err) {
return graphRequestFailedResponse(functionId, err);
}
},
);
Expand All @@ -1078,11 +1092,13 @@ export function registerApiTriggers(
async (req: ApiRequest): Promise<Response> => {
const authErr = checkAuth(req, secret);
if (authErr) return authErr;
if (!isGraphExtractionEnabled()) return graphDisabledResponse();
const functionId = "mem::graph-stats";
try {
const result = await sdk.trigger({ function_id: "mem::graph-stats", payload: {} });
const result = await sdk.trigger({ function_id: functionId, payload: {} });
return { status_code: 200, body: result };
} catch {
return graphDisabledResponse();
} catch (err) {
return graphRequestFailedResponse(functionId, err);
}
},
);
Expand All @@ -1096,6 +1112,7 @@ export function registerApiTriggers(
async (req: ApiRequest<{ observations: unknown[] }>): Promise<Response> => {
const authErr = checkAuth(req, secret);
if (authErr) return authErr;
if (!isGraphExtractionEnabled()) return graphDisabledResponse();
if (
!Array.isArray(req.body?.observations) ||
req.body.observations.length === 0
Expand All @@ -1105,11 +1122,12 @@ export function registerApiTriggers(
body: { error: "observations array is required" },
};
}
const functionId = "mem::graph-extract";
try {
const result = await sdk.trigger({ function_id: "mem::graph-extract", payload: req.body });
const result = await sdk.trigger({ function_id: functionId, payload: req.body });
return { status_code: 200, body: result };
} catch {
return graphDisabledResponse();
} catch (err) {
return graphRequestFailedResponse(functionId, err);
}
},
);
Expand Down
107 changes: 107 additions & 0 deletions test/api-graph.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { registerApiTriggers } from "../src/triggers/api.js";

type ApiResponse = {
status_code: number;
body: unknown;
};

type ApiHandler = (req: {
body?: unknown;
headers?: Record<string, string>;
}) => Promise<ApiResponse>;

function createSdk(
triggerImpl: (input: { function_id: string; payload: unknown }) => Promise<unknown>,
) {
const functions = new Map<string, ApiHandler>();
const sdk = {
registerFunction: vi.fn((id: string, handler: ApiHandler) => {
functions.set(id, handler);
}),
registerTrigger: vi.fn(),
trigger: vi.fn(triggerImpl),
};

return { sdk, functions };
}

describe("graph REST endpoints", () => {
afterEach(() => {
vi.unstubAllEnvs();
});

it.each([
{ apiFunction: "api::graph-query", request: { headers: {} } },
{ apiFunction: "api::graph-stats", request: { headers: {} } },
{ apiFunction: "api::graph-extract", request: { headers: {} } },
])("returns disabled response for $apiFunction when graph extraction is off", async ({
apiFunction,
request,
}) => {
vi.stubEnv("GRAPH_EXTRACTION_ENABLED", "false");
const { sdk, functions } = createSdk(async () => ({ ok: true }));

registerApiTriggers(sdk as never, {} as never);
const response = await functions.get(apiFunction)!(request);

expect(response.status_code).toBe(503);
expect(response.body).toMatchObject({
error: "Knowledge graph not enabled",
flag: "GRAPH_EXTRACTION_ENABLED",
});
expect(sdk.trigger).not.toHaveBeenCalled();
});

it("returns graph stats when graph extraction is on", async () => {
vi.stubEnv("GRAPH_EXTRACTION_ENABLED", "true");
const stats = { success: true, nodes: 2, edges: 1 };
const { sdk, functions } = createSdk(async () => stats);

registerApiTriggers(sdk as never, {} as never);
const response = await functions.get("api::graph-stats")!({ headers: {} });

expect(response).toEqual({ status_code: 200, body: stats });
expect(sdk.trigger).toHaveBeenCalledWith({
function_id: "mem::graph-stats",
payload: {},
});
});

it.each([
{
apiFunction: "api::graph-query",
memFunction: "mem::graph-query",
request: { headers: {}, body: { query: "index" } },
},
{
apiFunction: "api::graph-stats",
memFunction: "mem::graph-stats",
request: { headers: {} },
},
{
apiFunction: "api::graph-extract",
memFunction: "mem::graph-extract",
request: { headers: {}, body: { observations: [{ id: "obs-1" }] } },
},
])("reports $memFunction trigger failures separately from disabled graph extraction", async ({
apiFunction,
memFunction,
request,
}) => {
vi.stubEnv("GRAPH_EXTRACTION_ENABLED", "true");
const { sdk, functions } = createSdk(async () => {
throw new Error(`iii::engine Function not found: ${memFunction}`);
});

registerApiTriggers(sdk as never, {} as never);
const response = await functions.get(apiFunction)!(request);

expect(response.status_code).toBe(500);
expect(response.body).toMatchObject({
error: "Knowledge graph request failed",
functionId: memFunction,
message: `iii::engine Function not found: ${memFunction}`,
});
});
});
Loading