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
37 changes: 37 additions & 0 deletions direct-maintain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { getDb, closeDb } from "./src/store/db.js";
import { runMaintenance } from "./src/graph/maintenance.js";
import { DEFAULT_CONFIG } from "./src/types.js";
import { createCompleteFn } from "./src/engine/llm.js";
import { createEmbedFn } from "./src/engine/embed.js";
import { readFileSync } from "fs";

const envPath = `${process.env.HOME}/.hermes/graph-memory.env`;
const envText = readFileSync(envPath, "utf-8");
const getEnv = (key: string) => {
const m = envText.match(new RegExp(`${key}=(.+)`));
return m ? m[1].trim() : undefined;
};

const dbPath = `${process.env.HOME}/.hermes/graph-memory.db`;

const llm = createCompleteFn({
apiKey: getEnv("GRAPH_MEMORY_LLM_API_KEY"),
baseURL: getEnv("GRAPH_MEMORY_LLM_BASE_URL"),
model: getEnv("GRAPH_MEMORY_LLM_MODEL"),
});

const embed = await createEmbedFn({
apiKey: getEnv("GRAPH_MEMORY_EMBED_API_KEY"),
baseURL: getEnv("GRAPH_MEMORY_EMBED_BASE_URL"),
model: getEnv("GRAPH_MEMORY_EMBED_MODEL"),
});

const cfg = { ...DEFAULT_CONFIG, dbPath };
const db = getDb(dbPath);

console.log("Starting direct maintenance...");
const start = Date.now();
const result = await runMaintenance(db, cfg, llm, embed || undefined);
console.log(`Done in ${Date.now() - start}ms`);
console.log(JSON.stringify(result, null, 2));
closeDb();
28 changes: 15 additions & 13 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,24 @@
*/
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { Type } from "@sinclair/typebox";
import { getDb } from "./src/store/db.ts";
import { getDb } from "./src/store/db.js";
import {
saveMessage, getUnextracted,
markExtracted,
upsertNode, upsertEdge, findByName,
getBySession, edgesFrom, edgesTo,
deprecate, getStats,
} from "./src/store/store.ts";
import { createCompleteFn } from "./src/engine/llm.ts";
import { createEmbedFn } from "./src/engine/embed.ts";
import { Recaller } from "./src/recaller/recall.ts";
import { Extractor } from "./src/extractor/extract.ts";
import { assembleContext } from "./src/format/assemble.ts";
import { sanitizeToolUseResultPairing } from "./src/format/transcript-repair.ts";
import { runMaintenance } from "./src/graph/maintenance.ts";
import { invalidateGraphCache, computeGlobalPageRank } from "./src/graph/pagerank.ts";
import { detectCommunities } from "./src/graph/community.ts";
import { DEFAULT_CONFIG, type GmConfig } from "./src/types.ts";
} from "./src/store/store.js";
import { createCompleteFn } from "./src/engine/llm.js";
import { createEmbedFn } from "./src/engine/embed.js";
import { Recaller } from "./src/recaller/recall.js";
import { Extractor } from "./src/extractor/extract.js";
import { assembleContext } from "./src/format/assemble.js";
import { sanitizeToolUseResultPairing } from "./src/format/transcript-repair.js";
import { runMaintenance } from "./src/graph/maintenance.js";
import { invalidateGraphCache, computeGlobalPageRank } from "./src/graph/pagerank.js";
import { detectCommunities } from "./src/graph/community.js";
import { DEFAULT_CONFIG, type GmConfig } from "./src/types.js";

// ─── 从 OpenClaw config 读 provider/model ────────────────────

Expand Down Expand Up @@ -147,10 +147,12 @@ const graphMemoryPlugin = {
recaller.setEmbedFn(fn);
api.logger.info("[graph-memory] vector search ready");
} else {
recaller.setEmbedFn(null);
api.logger.info("[graph-memory] FTS5 search mode (配置 embedding 可启用语义搜索)");
}
})
.catch(() => {
recaller.setEmbedFn(null);
api.logger.info("[graph-memory] FTS5 search mode");
});

Expand Down Expand Up @@ -479,7 +481,7 @@ const graphMemoryPlugin = {
if (comm.communities.size > 0) {
(async () => {
try {
const { summarizeCommunities } = await import("./src/graph/community.ts");
const { summarizeCommunities } = await import("./src/graph/community.js");
const embedFn = (recaller as any).embed ?? undefined;
const summaries = await summarizeCommunities(db, comm.communities, llm, embedFn);
api.logger.info(
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "graph-memory",
"version": "1.5.8",
"description": "Knowledge Graph Memory Engine for OpenClaw — with Personalized PageRank, community detection, and vector dedup",
"main": "index.ts",
"main": "dist/index.js",
"type": "module",
"scripts": {
"build": "tsc",
Expand All @@ -24,7 +24,7 @@
},
"openclaw": {
"extensions": [
"./index.ts"
"./dist/index.js"
],
"hooks": {}
}
Expand Down
2 changes: 1 addition & 1 deletion src/engine/embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* 内置:429/5xx 重试 3 次 + 10s 超时
*/

import type { EmbeddingConfig } from "../types.ts";
import type { EmbeddingConfig } from "../types.js";

export type EmbedFn = (text: string) => Promise<number[]>;

Expand Down
52 changes: 43 additions & 9 deletions src/engine/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,24 +44,58 @@ async function fetchRetry(url: string, init: RequestInit, retries = 3, timeoutMs
throw new Error("[graph-memory] fetch failed after retries");
}

// ─── 从 model 字符串解析 provider ────────────────────────────

function resolveProviderModel(rawModel?: string): { provider: string; model: string } {
const raw = rawModel ?? "anthropic/claude-haiku-4-5-20251001";
if (raw.includes("/")) {
const [provider, ...rest] = raw.split("/");
const model = rest.join("/").trim();
if (provider?.trim() && model) {
return { provider: provider.trim(), model };
}
}
return { provider: "anthropic", model: raw };
}

// ─── CompleteFn 工厂 ────────────────────────────────────────

export function createCompleteFn(
provider: string,
model: string,
providerOrConfig: string | LlmConfig,
model?: string,
llmConfig?: LlmConfig,
anthropicApiKey?: string,
): CompleteFn {
// 支持直接传入 LlmConfig 对象(如 direct-maintain.ts 的用法)
let provider: string;
let finalModel: string;
let finalLlmConfig: LlmConfig | undefined;
let finalAnthropicKey: string | undefined;

if (typeof providerOrConfig === "string") {
provider = providerOrConfig;
finalModel = model!;
finalLlmConfig = llmConfig;
finalAnthropicKey = anthropicApiKey;
} else {
const cfg = providerOrConfig;
finalLlmConfig = cfg;
finalAnthropicKey = undefined;
const resolved = resolveProviderModel(cfg.model);
provider = resolved.provider;
finalModel = resolved.model;
}

return async (system, user) => {
// ── 路径 A(优先):pluginConfig.llm 直接调 OpenAI 兼容 API ──
if (llmConfig?.apiKey && llmConfig?.baseURL) {
const baseURL = llmConfig.baseURL.replace(/\/+$/, "");
const llmModel = llmConfig.model ?? model;
if (finalLlmConfig?.apiKey && finalLlmConfig?.baseURL) {
const baseURL = finalLlmConfig.baseURL.replace(/\/+$/, "");
const llmModel = finalLlmConfig.model ?? finalModel;
const res = await fetchRetry(`${baseURL}/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${llmConfig.apiKey}`,
"Authorization": `Bearer ${finalLlmConfig.apiKey}`,
},
body: JSON.stringify({
model: llmModel,
Expand All @@ -83,15 +117,15 @@ export function createCompleteFn(
}

// ── 路径 B:Anthropic API ──────────────────────────────
if (!anthropicApiKey) {
if (!finalAnthropicKey) {
throw new Error(
"[graph-memory] No LLM available. 在 openclaw.json 的 graph-memory config 中配置 llm.apiKey + llm.baseURL",
);
}
const res = await fetchRetry("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: { "Content-Type": "application/json", "x-api-key": anthropicApiKey, "anthropic-version": "2023-06-01" },
body: JSON.stringify({ model: llmConfig?.model ?? model, max_tokens: 4096, system, messages: [{ role: "user", content: user }] }),
headers: { "Content-Type": "application/json", "x-api-key": finalAnthropicKey, "anthropic-version": "2023-06-01" },
body: JSON.stringify({ model: finalLlmConfig?.model ?? finalModel, max_tokens: 4096, system, messages: [{ role: "user", content: user }] }),
});
if (!res.ok) throw new Error(`[graph-memory] Anthropic API ${res.status}`);
const data = await res.json() as any;
Expand Down
4 changes: 2 additions & 2 deletions src/extractor/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* Email: Wywelljob@gmail.com
*/

import type { GmConfig, ExtractionResult, FinalizeResult } from "../types.ts";
import type { CompleteFn } from "../engine/llm.ts";
import type { GmConfig, ExtractionResult, FinalizeResult } from "../types.js";
import type { CompleteFn } from "../engine/llm.js";

// ─── 节点/边合法值 ──────────────────────────────────────────────

Expand Down
4 changes: 2 additions & 2 deletions src/format/assemble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
*/

import { DatabaseSync, type DatabaseSyncInstance } from "@photostructure/sqlite";
import type { GmNode, GmEdge } from "../types.ts";
import { getCommunitySummary, getEpisodicMessages } from "../store/store.ts";
import type { GmNode, GmEdge } from "../types.js";
import { getCommunitySummary, getEpisodicMessages } from "../store/store.js";

const CHARS_PER_TOKEN = 3;

Expand Down
8 changes: 4 additions & 4 deletions src/graph/community.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
*/

import { DatabaseSync, type DatabaseSyncInstance } from "@photostructure/sqlite";
import { updateCommunities } from "../store/store.ts";
import { updateCommunities } from "../store/store.js";

export interface CommunityResult {
labels: Map<string, string>;
Expand Down Expand Up @@ -166,9 +166,9 @@ export function getCommunityPeers(db: DatabaseSyncInstance, nodeId: string, limi

// ─── 社区描述生成 ────────────────────────────────────────────

import type { CompleteFn } from "../engine/llm.ts";
import type { EmbedFn } from "../engine/embed.ts";
import { upsertCommunitySummary, pruneCommunitySummaries } from "../store/store.ts";
import type { CompleteFn } from "../engine/llm.js";
import type { EmbedFn } from "../engine/embed.js";
import { upsertCommunitySummary, pruneCommunitySummaries } from "../store/store.js";

const COMMUNITY_SUMMARY_SYS = `你是知识图谱摘要引擎。根据节点列表,用简短的描述概括这组节点的主题领域。
要求:
Expand Down
4 changes: 2 additions & 2 deletions src/graph/dedup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
*/

import { DatabaseSync, type DatabaseSyncInstance } from "@photostructure/sqlite";
import type { GmConfig, GmNode } from "../types.ts";
import { findById, mergeNodes, getAllVectors } from "../store/store.ts";
import type { GmConfig, GmNode } from "../types.js";
import { findById, mergeNodes, getAllVectors } from "../store/store.js";

export interface DuplicatePair {
nodeA: string;
Expand Down
12 changes: 6 additions & 6 deletions src/graph/maintenance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
*/

import { DatabaseSync, type DatabaseSyncInstance } from "@photostructure/sqlite";
import type { GmConfig } from "../types.ts";
import type { CompleteFn } from "../engine/llm.ts";
import type { EmbedFn } from "../engine/embed.ts";
import { computeGlobalPageRank, invalidateGraphCache, type GlobalPageRankResult } from "./pagerank.ts";
import { detectCommunities, summarizeCommunities, type CommunityResult } from "./community.ts";
import { dedup, type DedupResult } from "./dedup.ts";
import type { GmConfig } from "../types.js";
import type { CompleteFn } from "../engine/llm.js";
import type { EmbedFn } from "../engine/embed.js";
import { computeGlobalPageRank, invalidateGraphCache, type GlobalPageRankResult } from "./pagerank.js";
import { detectCommunities, summarizeCommunities, type CommunityResult } from "./community.js";
import { dedup, type DedupResult } from "./dedup.js";

export interface MaintenanceResult {
dedup: DedupResult;
Expand Down
4 changes: 2 additions & 2 deletions src/graph/pagerank.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
*/

import { DatabaseSync, type DatabaseSyncInstance } from "@photostructure/sqlite";
import type { GmConfig } from "../types.ts";
import { updatePageranks } from "../store/store.ts";
import type { GmConfig } from "../types.js";
import { updatePageranks } from "../store/store.js";

// ─── 图结构缓存(避免每次 recall 都查 SQL) ─────────────────

Expand Down
13 changes: 13 additions & 0 deletions src/openclaw-stub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Stub for openclaw/plugin-sdk to allow tsc build without the peer dependency
export interface OpenClawPluginApi {
pluginConfig?: unknown;
config?: unknown;
logger: {
info: (msg: string) => void;
error: (msg: string) => void;
warn: (msg: string) => void;
};
on: (event: string, handler: (...args: any[]) => any) => void;
registerContextEngine: (name: string, factory: () => any) => void;
registerTool: (factory: (ctx: any) => any, meta?: { name?: string }) => void;
}
24 changes: 17 additions & 7 deletions src/recaller/recall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,35 @@

import { DatabaseSync, type DatabaseSyncInstance } from "@photostructure/sqlite";
import { createHash } from "crypto";
import type { GmConfig, RecallResult, GmNode, GmEdge } from "../types.ts";
import type { EmbedFn } from "../engine/embed.ts";
import type { GmConfig, RecallResult, GmNode, GmEdge } from "../types.js";
import type { EmbedFn } from "../engine/embed.js";
import {
searchNodes, vectorSearchWithScore,
graphWalk, communityRepresentatives,
communityVectorSearch, nodesByCommunityIds,
saveVector, getVectorHash,
} from "../store/store.ts";
import { getCommunityPeers } from "../graph/community.ts";
import { personalizedPageRank } from "../graph/pagerank.ts";
} from "../store/store.js";
import { getCommunityPeers } from "../graph/community.js";
import { personalizedPageRank } from "../graph/pagerank.js";

export class Recaller {
private embed: EmbedFn | null = null;
private embedReady: Promise<void>;
private resolveReady!: () => void;

constructor(private db: DatabaseSyncInstance, private cfg: GmConfig) {}
constructor(private db: DatabaseSyncInstance, private cfg: GmConfig) {
this.embedReady = new Promise((resolve) => {
this.resolveReady = resolve;
});
}

setEmbedFn(fn: EmbedFn): void { this.embed = fn; }
setEmbedFn(fn: EmbedFn | null): void {
this.embed = fn;
this.resolveReady();
}

async recall(query: string): Promise<RecallResult> {
await this.embedReady;
const limit = this.cfg.recallMaxNodes;

// ── 两条路径各自独立跑满,不分配额 ──────────────────
Expand Down
2 changes: 1 addition & 1 deletion src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { DatabaseSync, type DatabaseSyncInstance } from "@photostructure/sqlite";
import { createHash } from "crypto";
import type { GmNode, GmEdge, EdgeType, NodeType, Signal } from "../types.ts";
import type { GmNode, GmEdge, EdgeType, NodeType, Signal } from "../types.js";

// ─── 工具 ─────────────────────────────────────────────────────

Expand Down
10 changes: 5 additions & 5 deletions test/assemble.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@

import { describe, it, expect, beforeEach } from "vitest";
import { DatabaseSync, type DatabaseSyncInstance } from "@photostructure/sqlite";
import { createTestDb, insertNode, insertEdge } from "./helpers.ts";
import { assembleContext, buildSystemPromptAddition } from "../src/format/assemble.ts";
import { sanitizeToolUseResultPairing } from "../src/format/transcript-repair.ts";
import { findById } from "../src/store/store.ts";
import type { GmNode, GmEdge } from "../src/types.ts";
import { createTestDb, insertNode, insertEdge } from "./helpers.js";
import { assembleContext, buildSystemPromptAddition } from "../src/format/assemble.js";
import { sanitizeToolUseResultPairing } from "../src/format/transcript-repair.js";
import { findById } from "../src/store/store.js";
import type { GmNode, GmEdge } from "../src/types.js";

let db: DatabaseSyncInstance;

Expand Down
6 changes: 3 additions & 3 deletions test/extract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
*/

import { describe, it, expect } from "vitest";
import { Extractor } from "../src/extractor/extract.ts";
import { DEFAULT_CONFIG } from "../src/types.ts";
import type { ExtractionResult, FinalizeResult } from "../src/types.ts";
import { Extractor } from "../src/extractor/extract.js";
import { DEFAULT_CONFIG } from "../src/types.js";
import type { ExtractionResult, FinalizeResult } from "../src/types.js";

// ─── Mock LLM:直接返回预设 JSON ────────────────────────────────

Expand Down
Loading