refactor: adapt to WaterFlow framework - zero source changes required
- Remove duplicate platform layer and tool interface definitions - Add BFS breadth-first search with depth annotation (sync from main) - Use WaterFlow platform.fs for binary storage instead of custom storage - Add installTrulyMEM() one-line registration function - Update SKILL.md with 10 complete operations - Add waterflow.d.ts type declarations - Update bilingual README with simplified installation guide - Build passes with 0 errors
This commit is contained in:
38
ts/src/runtime/core/graph_memory/config.ts
Normal file
38
ts/src/runtime/core/graph_memory/config.ts
Normal file
@ -0,0 +1,38 @@
|
||||
export interface TrulyMEMConfig {
|
||||
dbPath: string;
|
||||
autoSave: boolean;
|
||||
debug: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_CONFIG: TrulyMEMConfig = {
|
||||
dbPath: '.trulymem/graph_memory.db',
|
||||
autoSave: true,
|
||||
debug: false
|
||||
};
|
||||
|
||||
let globalConfig: TrulyMEMConfig = { ...DEFAULT_CONFIG };
|
||||
|
||||
export function initConfig(config: Partial<TrulyMEMConfig> = {}): TrulyMEMConfig {
|
||||
globalConfig = { ...DEFAULT_CONFIG, ...config };
|
||||
return globalConfig;
|
||||
}
|
||||
|
||||
export function getConfig(): TrulyMEMConfig {
|
||||
return globalConfig;
|
||||
}
|
||||
|
||||
export function setDbPath(dbPath: string): void {
|
||||
globalConfig.dbPath = dbPath;
|
||||
}
|
||||
|
||||
export function getDbPath(): string {
|
||||
return globalConfig.dbPath;
|
||||
}
|
||||
|
||||
export function setAutoSave(autoSave: boolean): void {
|
||||
globalConfig.autoSave = autoSave;
|
||||
}
|
||||
|
||||
export function isAutoSave(): boolean {
|
||||
return globalConfig.autoSave;
|
||||
}
|
||||
@ -1,53 +1,159 @@
|
||||
import initSqlJs, { type Database as SqlJsDatabase } from 'sql.js';
|
||||
import type { Entity, Relation, RecallParams, CommitParams, PurgeParams, RecallResult, CommitResult, PurgeResult, MemoryStats } from './types';
|
||||
import { getConfig } from './config';
|
||||
|
||||
export class GraphDatabase {
|
||||
private entities: Map<string, Entity> = new Map();
|
||||
private relations: Map<string, Relation> = new Map();
|
||||
private db: SqlJsDatabase | null = null;
|
||||
private sessionId: string;
|
||||
private initPromise: Promise<void> | null = null;
|
||||
private _platform: any = null;
|
||||
|
||||
constructor(sessionId?: string) {
|
||||
this.sessionId = sessionId || `session-${Date.now()}`;
|
||||
this.initPromise = this.initDatabase();
|
||||
}
|
||||
|
||||
private async initDatabase(): Promise<void> {
|
||||
const SQL = await initSqlJs();
|
||||
const { getPlatform } = await import('waterflow/platform');
|
||||
this._platform = getPlatform();
|
||||
|
||||
try {
|
||||
const dbPath = this._platform.path.join(this._platform.getCwd(), getConfig().dbPath);
|
||||
const exists = await this._platform.fs.exists(dbPath);
|
||||
if (exists) {
|
||||
const data = await this._platform.fs.readFile(dbPath, { encoding: 'binary' });
|
||||
this.db = new SQL.Database(data as Uint8Array);
|
||||
} else {
|
||||
this.db = new SQL.Database();
|
||||
}
|
||||
} catch {
|
||||
this.db = new SQL.Database();
|
||||
}
|
||||
|
||||
this.createTables();
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
if (!this.db || !this._platform) return;
|
||||
|
||||
try {
|
||||
const platform = this._platform;
|
||||
const dbPath = platform.path.join(platform.getCwd(), getConfig().dbPath);
|
||||
const dir = platform.path.dirname(dbPath);
|
||||
const dirExists = await platform.fs.exists(dir);
|
||||
if (!dirExists) {
|
||||
await platform.fs.mkdir(dir, true);
|
||||
}
|
||||
const data = this.db.export();
|
||||
await platform.fs.writeFile(dbPath, new Uint8Array(data), { encoding: 'binary' });
|
||||
} catch (error) {
|
||||
this._platform.getLogger().error(`[GraphDatabase] Save failed: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private createTables(): void {
|
||||
if (!this.db) return;
|
||||
this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS entities (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT DEFAULT 'unknown',
|
||||
mention_count INTEGER DEFAULT 1,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS relations (
|
||||
id TEXT PRIMARY KEY,
|
||||
source_id TEXT NOT NULL,
|
||||
target_id TEXT NOT NULL,
|
||||
relation_type TEXT NOT NULL,
|
||||
confidence REAL DEFAULT 1.0,
|
||||
status TEXT DEFAULT 'active',
|
||||
session_id TEXT NOT NULL,
|
||||
turn_id INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
date_bucket TEXT NOT NULL,
|
||||
FOREIGN KEY (source_id) REFERENCES entities(id),
|
||||
FOREIGN KEY (target_id) REFERENCES entities(id)
|
||||
)
|
||||
`);
|
||||
this.db.run(`CREATE INDEX IF NOT EXISTS idx_entity_name ON entities(name)`);
|
||||
this.db.run(`CREATE INDEX IF NOT EXISTS idx_entity_type ON entities(type)`);
|
||||
this.db.run(`CREATE INDEX IF NOT EXISTS idx_relation_source ON relations(source_id)`);
|
||||
this.db.run(`CREATE INDEX IF NOT EXISTS idx_relation_target ON relations(target_id)`);
|
||||
this.db.run(`CREATE INDEX IF NOT EXISTS idx_relation_type ON relations(relation_type)`);
|
||||
this.db.run(`CREATE INDEX IF NOT EXISTS idx_relation_status ON relations(status)`);
|
||||
}
|
||||
|
||||
private ensureInit(): void {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
}
|
||||
|
||||
async recall(params: RecallParams): Promise<RecallResult> {
|
||||
const { queryIntent, seedEntities, sessionFilter } = params;
|
||||
await this.initPromise;
|
||||
this.ensureInit();
|
||||
const { queryIntent, seedEntities, depth = 2, sessionFilter } = params;
|
||||
const keywords = queryIntent.split(/[,\s]+/).filter(k => k.length > 0);
|
||||
const entities: Entity[] = [];
|
||||
const relations: Relation[] = [];
|
||||
const entityIds = new Set<string>();
|
||||
|
||||
for (const keyword of keywords) {
|
||||
const lowerKeyword = keyword.toLowerCase();
|
||||
for (const [_, entity] of this.entities) {
|
||||
if (entity.name.toLowerCase().includes(lowerKeyword)) {
|
||||
if (!entityIds.has(entity.id)) {
|
||||
entityIds.add(entity.id);
|
||||
entities.push(entity);
|
||||
if (!this.db) return { entities, relations, message: 'Database not ready' };
|
||||
|
||||
if (!keywords.length && !seedEntities?.length) {
|
||||
const rows = this.db.exec('SELECT * FROM entities ORDER BY mention_count DESC LIMIT 50');
|
||||
if (rows.length > 0) {
|
||||
const columns = rows[0].columns;
|
||||
for (const row of rows[0].values) {
|
||||
const obj = this.rowToObject(columns, row);
|
||||
const id = obj.id as string;
|
||||
entityIds.add(id);
|
||||
entities.push(this.rowToEntity(obj, 0));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const keyword of keywords) {
|
||||
const stmt = this.db.prepare('SELECT * FROM entities WHERE LOWER(name) LIKE ? LIMIT 100');
|
||||
stmt.bind([`%${keyword.toLowerCase()}%`]);
|
||||
while (stmt.step()) {
|
||||
const row = stmt.getAsObject();
|
||||
const id = row.id as string;
|
||||
if (!entityIds.has(id)) {
|
||||
entityIds.add(id);
|
||||
entities.push(this.rowToEntity(row, 0));
|
||||
}
|
||||
}
|
||||
stmt.free();
|
||||
}
|
||||
}
|
||||
|
||||
if (seedEntities && seedEntities.length > 0) {
|
||||
if (seedEntities?.length) {
|
||||
for (const seedName of seedEntities) {
|
||||
for (const [_, entity] of this.entities) {
|
||||
if (entity.name.toLowerCase() === seedName.toLowerCase()) {
|
||||
if (!entityIds.has(entity.id)) {
|
||||
entityIds.add(entity.id);
|
||||
entities.push(entity);
|
||||
}
|
||||
const stmt = this.db.prepare('SELECT * FROM entities WHERE LOWER(name) = ? LIMIT 1');
|
||||
stmt.bind([seedName.toLowerCase()]);
|
||||
if (stmt.step()) {
|
||||
const row = stmt.getAsObject();
|
||||
const id = row.id as string;
|
||||
if (!entityIds.has(id)) {
|
||||
entityIds.add(id);
|
||||
entities.push(this.rowToEntity(row, 0));
|
||||
}
|
||||
}
|
||||
stmt.free();
|
||||
}
|
||||
}
|
||||
|
||||
for (const [_, relation] of this.relations) {
|
||||
if (entityIds.has(relation.sourceId) || entityIds.has(relation.targetId)) {
|
||||
if (relation.status === 'active') {
|
||||
if (!sessionFilter || relation.sessionId === sessionFilter) {
|
||||
relations.push(relation);
|
||||
}
|
||||
}
|
||||
this.bfsExpand(entityIds, entities, relations, depth, sessionFilter);
|
||||
|
||||
for (const entity of entities) {
|
||||
if (entity.depth === undefined) {
|
||||
entity.depth = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,132 +164,268 @@ export class GraphDatabase {
|
||||
};
|
||||
}
|
||||
|
||||
private bfsExpand(
|
||||
seedIds: Set<string>,
|
||||
entities: Entity[],
|
||||
relations: Relation[],
|
||||
maxDepth: number,
|
||||
sessionFilter?: string
|
||||
): void {
|
||||
if (!this.db) return;
|
||||
|
||||
const visited = new Set(seedIds);
|
||||
let currentLayer = new Set(seedIds);
|
||||
const entityDepths: Record<string, number> = {};
|
||||
for (const id of seedIds) {
|
||||
entityDepths[id] = 0;
|
||||
}
|
||||
|
||||
for (let layer = 0; layer < maxDepth; layer++) {
|
||||
if (!currentLayer.size) break;
|
||||
|
||||
const placeholders = Array(currentLayer.size).fill('?').join(',');
|
||||
let sql = `
|
||||
SELECT r.id, r.source_id, r.target_id,
|
||||
e1.name as source_name, e2.name as target_name,
|
||||
r.relation_type, r.confidence, r.session_id,
|
||||
r.turn_id, r.created_at, r.updated_at, r.status, r.date_bucket
|
||||
FROM relations r
|
||||
JOIN entities e1 ON r.source_id = e1.id
|
||||
JOIN entities e2 ON r.target_id = e2.id
|
||||
WHERE (r.source_id IN (${placeholders}) OR r.target_id IN (${placeholders}))
|
||||
AND r.status = 'active'
|
||||
`;
|
||||
const params: (string | number)[] = [];
|
||||
for (const id of currentLayer) { params.push(id); }
|
||||
for (const id of currentLayer) { params.push(id); }
|
||||
if (sessionFilter) {
|
||||
sql += ` AND r.session_id = ?`;
|
||||
params.push(sessionFilter);
|
||||
}
|
||||
|
||||
const stmt = this.db.prepare(sql);
|
||||
stmt.bind(params);
|
||||
|
||||
const nextLayer = new Set<string>();
|
||||
const layerRelations: Relation[] = [];
|
||||
|
||||
while (stmt.step()) {
|
||||
const row = stmt.getAsObject();
|
||||
const sourceId = row.source_id as string;
|
||||
const targetId = row.target_id as string;
|
||||
const sourceDepth = entityDepths[sourceId] ?? layer;
|
||||
const targetDepth = entityDepths[targetId] ?? layer;
|
||||
const relationDepth = Math.max(sourceDepth, targetDepth) + 1;
|
||||
|
||||
layerRelations.push({
|
||||
id: row.id as string,
|
||||
sourceId,
|
||||
targetId,
|
||||
relationType: row.relation_type as string,
|
||||
confidence: row.confidence as number,
|
||||
status: row.status as Relation['status'],
|
||||
sessionId: row.session_id as string,
|
||||
turnId: row.turn_id as number,
|
||||
createdAt: new Date(row.created_at as string),
|
||||
updatedAt: new Date(row.updated_at as string),
|
||||
dateBucket: row.date_bucket as string,
|
||||
depth: relationDepth
|
||||
});
|
||||
|
||||
if (!visited.has(sourceId)) {
|
||||
visited.add(sourceId);
|
||||
nextLayer.add(sourceId);
|
||||
entityDepths[sourceId] = layer + 1;
|
||||
}
|
||||
if (!visited.has(targetId)) {
|
||||
visited.add(targetId);
|
||||
nextLayer.add(targetId);
|
||||
entityDepths[targetId] = layer + 1;
|
||||
}
|
||||
}
|
||||
stmt.free();
|
||||
|
||||
relations.push(...layerRelations);
|
||||
|
||||
if (nextLayer.size) {
|
||||
const placeholders = Array(nextLayer.size).fill('?').join(',');
|
||||
const entityStmt = this.db.prepare(
|
||||
`SELECT * FROM entities WHERE id IN (${placeholders})`
|
||||
);
|
||||
entityStmt.bind(Array.from(nextLayer));
|
||||
while (entityStmt.step()) {
|
||||
const row = entityStmt.getAsObject();
|
||||
const id = row.id as string;
|
||||
entities.push(this.rowToEntity(row, entityDepths[id] ?? layer + 1));
|
||||
}
|
||||
entityStmt.free();
|
||||
}
|
||||
|
||||
currentLayer = nextLayer;
|
||||
}
|
||||
}
|
||||
|
||||
async commit(params: CommitParams): Promise<CommitResult> {
|
||||
await this.initPromise;
|
||||
this.ensureInit();
|
||||
const { triplets, sessionId, turnId } = params;
|
||||
let createdEntities = 0;
|
||||
let createdRelations = 0;
|
||||
|
||||
if (!this.db) return { createdEntities: 0, createdRelations: 0 };
|
||||
|
||||
for (const triplet of triplets) {
|
||||
const sourceId = this.upsertEntity(triplet.subject);
|
||||
const targetId = this.upsertEntity(triplet.object);
|
||||
|
||||
const relationId = this.generateId();
|
||||
const now = new Date();
|
||||
const relation: Relation = {
|
||||
id: relationId,
|
||||
sourceId,
|
||||
targetId,
|
||||
relationType: triplet.relation,
|
||||
confidence: triplet.confidence || 1.0,
|
||||
status: 'active',
|
||||
sessionId: sessionId || this.sessionId,
|
||||
turnId: turnId || 0,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
dateBucket: this.getDateBucket(now)
|
||||
};
|
||||
const now = new Date().toISOString();
|
||||
this.db.run(
|
||||
`INSERT INTO relations (id, source_id, target_id, relation_type, confidence, status, session_id, turn_id, created_at, updated_at, date_bucket)
|
||||
VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?)`,
|
||||
[relationId, sourceId, targetId, triplet.relation, triplet.confidence ?? 1.0, sessionId ?? this.sessionId, turnId ?? 0, now, now, new Date().toISOString().split('T')[0]]
|
||||
);
|
||||
|
||||
this.relations.set(relationId, relation);
|
||||
createdEntities += 2;
|
||||
createdRelations++;
|
||||
}
|
||||
|
||||
if (getConfig().autoSave) {
|
||||
await this.save();
|
||||
}
|
||||
|
||||
return { createdEntities, createdRelations };
|
||||
}
|
||||
|
||||
async purge(params: PurgeParams): Promise<PurgeResult> {
|
||||
await this.initPromise;
|
||||
this.ensureInit();
|
||||
const { criteria, mode = 'soft' } = params;
|
||||
let deleted = 0;
|
||||
|
||||
for (const [id, relation] of this.relations) {
|
||||
if (relation.status !== 'active') continue;
|
||||
if (!this.db) return { deleted: 0, mode };
|
||||
|
||||
if (!criteria) {
|
||||
continue;
|
||||
}
|
||||
const conditions: string[] = [];
|
||||
const queryParams: (string | number)[] = [];
|
||||
|
||||
let matches = true;
|
||||
if (criteria.subject) {
|
||||
const sourceEntity = this.entities.get(relation.sourceId);
|
||||
matches = sourceEntity?.name.toLowerCase() === criteria.subject.toLowerCase();
|
||||
}
|
||||
if (matches && criteria.target) {
|
||||
const targetEntity = this.entities.get(relation.targetId);
|
||||
matches = targetEntity?.name.toLowerCase() === criteria.target.toLowerCase();
|
||||
}
|
||||
if (matches && criteria.relation) {
|
||||
matches = relation.relationType.toLowerCase() === criteria.relation.toLowerCase();
|
||||
}
|
||||
if (matches && criteria.sessionId) {
|
||||
matches = relation.sessionId === criteria.sessionId;
|
||||
if (criteria?.subject) {
|
||||
const stmt = this.db.prepare('SELECT id FROM entities WHERE LOWER(name) = ?');
|
||||
stmt.bind([criteria.subject.toLowerCase()]);
|
||||
if (stmt.step()) {
|
||||
const row = stmt.getAsObject();
|
||||
conditions.push(`source_id = ?`);
|
||||
queryParams.push(row.id as string);
|
||||
}
|
||||
stmt.free();
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
if (mode === 'hard') {
|
||||
this.relations.delete(id);
|
||||
} else {
|
||||
relation.status = 'deleted';
|
||||
relation.updatedAt = new Date();
|
||||
}
|
||||
deleted++;
|
||||
if (criteria?.target) {
|
||||
const stmt = this.db.prepare('SELECT id FROM entities WHERE LOWER(name) = ?');
|
||||
stmt.bind([criteria.target.toLowerCase()]);
|
||||
if (stmt.step()) {
|
||||
const row = stmt.getAsObject();
|
||||
conditions.push(`target_id = ?`);
|
||||
queryParams.push(row.id as string);
|
||||
}
|
||||
stmt.free();
|
||||
}
|
||||
|
||||
if (criteria?.relation) {
|
||||
conditions.push(`relation_type = ?`);
|
||||
queryParams.push(criteria.relation);
|
||||
}
|
||||
|
||||
if (!conditions.length) {
|
||||
return { deleted: 0, mode, message: '无删除条件' };
|
||||
}
|
||||
|
||||
const whereClause = conditions.join(' AND ');
|
||||
|
||||
const countStmt = this.db.prepare(`SELECT COUNT(*) as cnt FROM relations WHERE ${whereClause} AND status = 'active'`);
|
||||
countStmt.bind(queryParams);
|
||||
if (countStmt.step()) {
|
||||
const row = countStmt.getAsObject();
|
||||
deleted = row.cnt as number;
|
||||
}
|
||||
countStmt.free();
|
||||
|
||||
if (mode === 'hard') {
|
||||
this.db.run(`DELETE FROM relations WHERE ${whereClause} AND status = 'active'`, queryParams);
|
||||
} else {
|
||||
this.db.run(
|
||||
`UPDATE relations SET status = 'deleted', updated_at = ? WHERE ${whereClause} AND status = 'active'`,
|
||||
[new Date().toISOString(), ...queryParams]
|
||||
);
|
||||
}
|
||||
|
||||
if (getConfig().autoSave && deleted > 0) {
|
||||
await this.save();
|
||||
}
|
||||
|
||||
return { deleted, mode };
|
||||
}
|
||||
|
||||
async introspect(): Promise<MemoryStats> {
|
||||
let entityCount = 0;
|
||||
for (const [_, entity] of this.entities) {
|
||||
if (!this.isEntityDeleted(entity.id)) entityCount++;
|
||||
}
|
||||
await this.initPromise;
|
||||
this.ensureInit();
|
||||
|
||||
let relationCount = 0;
|
||||
for (const [_, relation] of this.relations) {
|
||||
if (relation.status === 'active') relationCount++;
|
||||
}
|
||||
if (!this.db) return { entityCount: 0, relationCount: 0, sessionId: this.sessionId };
|
||||
|
||||
const entityCount = (this.db.exec('SELECT COUNT(*) FROM entities')[0]?.values[0]?.[0] as number) ?? 0;
|
||||
const relationCount = (this.db.exec("SELECT COUNT(*) FROM relations WHERE status = 'active'")[0]?.values[0]?.[0] as number) ?? 0;
|
||||
|
||||
return { entityCount, relationCount, sessionId: this.sessionId };
|
||||
}
|
||||
|
||||
private upsertEntity(name: string): string {
|
||||
for (const [id, entity] of this.entities) {
|
||||
if (entity.name === name && !this.isEntityDeleted(id)) {
|
||||
entity.mentionCount++;
|
||||
entity.updatedAt = new Date();
|
||||
return id;
|
||||
}
|
||||
if (!this.db) return this.generateId();
|
||||
|
||||
const existingStmt = this.db.prepare('SELECT id, mention_count FROM entities WHERE LOWER(name) = ?');
|
||||
existingStmt.bind([name.toLowerCase()]);
|
||||
if (existingStmt.step()) {
|
||||
const row = existingStmt.getAsObject();
|
||||
const id = row.id as string;
|
||||
this.db.run('UPDATE entities SET mention_count = ?, updated_at = ? WHERE id = ?', [(row.mention_count as number) + 1, new Date().toISOString(), id]);
|
||||
existingStmt.free();
|
||||
return id;
|
||||
}
|
||||
existingStmt.free();
|
||||
|
||||
const id = this.generateId();
|
||||
const now = new Date();
|
||||
const entity: Entity = {
|
||||
id,
|
||||
name,
|
||||
type: 'unknown',
|
||||
mentionCount: 1,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
};
|
||||
this.entities.set(id, entity);
|
||||
const now = new Date().toISOString();
|
||||
this.db.run(
|
||||
'INSERT INTO entities (id, name, type, mention_count, created_at, updated_at) VALUES (?, ?, ?, 1, ?, ?)',
|
||||
[id, name, 'unknown', now, now]
|
||||
);
|
||||
return id;
|
||||
}
|
||||
|
||||
private isEntityDeleted(entityId: string): boolean {
|
||||
for (const [_, relation] of this.relations) {
|
||||
if ((relation.sourceId === entityId || relation.targetId === entityId) && relation.status === 'deleted') {
|
||||
return true;
|
||||
}
|
||||
private rowToEntity(row: Record<string, unknown>, depth?: number): Entity {
|
||||
const entity: Entity = {
|
||||
id: row.id as string,
|
||||
name: row.name as string,
|
||||
type: (row.type as string) ?? 'unknown',
|
||||
mentionCount: row.mention_count as number,
|
||||
createdAt: new Date(row.created_at as string),
|
||||
updatedAt: new Date(row.updated_at as string)
|
||||
};
|
||||
if (depth !== undefined) {
|
||||
entity.depth = depth;
|
||||
}
|
||||
return false;
|
||||
return entity;
|
||||
}
|
||||
|
||||
private rowToObject(columns: string[], values: unknown[]): Record<string, unknown> {
|
||||
const obj: Record<string, unknown> = {};
|
||||
columns.forEach((col, i) => { obj[col] = values[i]; });
|
||||
return obj;
|
||||
}
|
||||
|
||||
private generateId(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
private getDateBucket(date: Date): string {
|
||||
return date.toISOString().split('T')[0] ?? '';
|
||||
if (this._platform?.globals?.randomUUID) {
|
||||
return this._platform.globals.randomUUID();
|
||||
}
|
||||
return `ent-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
||||
}
|
||||
|
||||
setSessionId(sessionId: string): void {
|
||||
@ -193,4 +435,11 @@ export class GraphDatabase {
|
||||
getSessionId(): string {
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (this.db) {
|
||||
this.db.close();
|
||||
this.db = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from './types';
|
||||
export * from './config';
|
||||
export * from './graph_database';
|
||||
export * from './memory_service';
|
||||
export * from './memory_service';
|
||||
@ -5,6 +5,7 @@ export interface Entity {
|
||||
mentionCount: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
depth?: number; // BFS 搜索深度标注
|
||||
}
|
||||
|
||||
export type RelationStatus = 'active' | 'deleted' | 'archived' | 'superseded';
|
||||
@ -21,6 +22,7 @@ export interface Relation {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
dateBucket: string;
|
||||
depth?: number; // BFS 搜索深度标注
|
||||
}
|
||||
|
||||
export interface Triplet {
|
||||
@ -71,6 +73,7 @@ export interface CommitResult {
|
||||
export interface PurgeResult {
|
||||
deleted: number;
|
||||
mode: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export type TaskState = '进行中' | '已完成' | '已暂停' | '已取消';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Tool, ToolCategory, PermissionLevel, ToolInputSchema, ToolExecutionContext, ToolOutput } from '../tool_interface';
|
||||
import type { Tool, ToolCategory, PermissionLevel, ToolInputSchema, ToolOutput, ToolInput, ToolExecutionContext } from 'waterflow/runtime/core/tools/tool_interface';
|
||||
import { GraphDatabase } from '../../graph_memory/graph_database';
|
||||
import { MemoryService } from '../../graph_memory/memory_service';
|
||||
|
||||
@ -8,7 +8,7 @@ const GRAPH_MEMORY_TOOL_DESCRIPTION = `图记忆工具 - 让 AI 拥有真正的
|
||||
|
||||
操作:
|
||||
- recall: 检索记忆
|
||||
- commit: 写入记忆
|
||||
- commit: 写入记忆
|
||||
- purge: 删除记忆
|
||||
- introspect: 查看状态
|
||||
- persona_update/clear: 人设管理
|
||||
@ -20,6 +20,7 @@ export class GraphMemoryTool implements Tool {
|
||||
readonly description = GRAPH_MEMORY_TOOL_DESCRIPTION;
|
||||
readonly category: ToolCategory = 'analysis';
|
||||
readonly permissionLevel: PermissionLevel = 'safe';
|
||||
readonly alwaysLoad = true;
|
||||
|
||||
readonly inputSchema: ToolInputSchema = {
|
||||
type: 'object',
|
||||
@ -100,14 +101,18 @@ export class GraphMemoryTool implements Tool {
|
||||
this.service = new MemoryService(this.db);
|
||||
}
|
||||
|
||||
async handler(params: Record<string, unknown>, _context: ToolExecutionContext): Promise<ToolOutput> {
|
||||
async handler(params: ToolInput, context: ToolExecutionContext): Promise<ToolOutput> {
|
||||
const action = params.action as string;
|
||||
const actionParams = params.params as Record<string, unknown>;
|
||||
const logger = context?.logger;
|
||||
|
||||
try {
|
||||
logger?.info(`[GraphMemoryTool] Executing action: ${action}`);
|
||||
const result = await this.executeAction(action, actionParams);
|
||||
logger?.info(`[GraphMemoryTool] Action ${action} completed successfully`);
|
||||
return JSON.stringify({ success: true, data: result }, null, 2);
|
||||
} catch (error) {
|
||||
logger?.error(`[GraphMemoryTool] Action ${action} failed:`, error);
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
error: {
|
||||
|
||||
19
ts/src/runtime/core/tools/builtin/index.ts
Normal file
19
ts/src/runtime/core/tools/builtin/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import type { Tool } from 'waterflow/runtime/core/tools/tool_interface';
|
||||
import type { Platform } from 'waterflow/platform/types';
|
||||
import { GraphMemoryTool, createGraphMemoryTool } from './graph_memory_tool';
|
||||
|
||||
export { GraphMemoryTool, createGraphMemoryTool };
|
||||
|
||||
export function registerGraphMemoryTool(
|
||||
registry: { register: (tool: Tool) => void },
|
||||
sessionId?: string
|
||||
): void {
|
||||
registry.register(createGraphMemoryTool(sessionId));
|
||||
}
|
||||
|
||||
export function installTrulyMEM(platform: Platform, sessionId?: string) {
|
||||
const { initializeToolRegistry } = require('waterflow/runtime/core/tools/builtin');
|
||||
const registry = initializeToolRegistry(platform);
|
||||
registerGraphMemoryTool(registry, sessionId);
|
||||
return registry;
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
export type ToolCategory = 'file' | 'code' | 'search' | 'execute' | 'network' | 'analysis' | 'generation' | 'communication' | 'mcp' | 'custom';
|
||||
|
||||
export type PermissionLevel = 'safe' | 'moderate' | 'dangerous' | 'restricted';
|
||||
|
||||
export type SchemaType = 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object';
|
||||
|
||||
export interface SchemaProperty {
|
||||
type: SchemaType;
|
||||
description: string;
|
||||
enum?: string[];
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
pattern?: string;
|
||||
default?: unknown;
|
||||
examples?: unknown[];
|
||||
items?: SchemaProperty;
|
||||
properties?: Record<string, SchemaProperty>;
|
||||
}
|
||||
|
||||
export interface ToolInputSchema {
|
||||
type: 'object';
|
||||
properties: Record<string, SchemaProperty>;
|
||||
required?: string[];
|
||||
additionalProperties?: boolean;
|
||||
}
|
||||
|
||||
export interface ToolOutputSchema {
|
||||
type: 'object';
|
||||
properties: Record<string, SchemaProperty>;
|
||||
format?: 'json' | 'text' | 'markdown' | 'binary';
|
||||
maxSize?: number;
|
||||
maxLines?: number;
|
||||
}
|
||||
|
||||
export type ToolInput = Record<string, unknown>;
|
||||
export type ToolOutput = string | Record<string, unknown> | void;
|
||||
|
||||
export interface ToolExecutionContext {
|
||||
toolCallId: string;
|
||||
workingDirectory: string;
|
||||
abortController: { signal: AbortSignal };
|
||||
config: { timeout?: number };
|
||||
logger: { info: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void; debug: (...args: unknown[]) => void };
|
||||
}
|
||||
|
||||
export type ToolHandler = (params: ToolInput, context: ToolExecutionContext) => Promise<ToolOutput>;
|
||||
|
||||
export interface Tool {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
readonly category: ToolCategory;
|
||||
readonly inputSchema: ToolInputSchema;
|
||||
readonly outputSchema?: ToolOutputSchema;
|
||||
readonly handler: ToolHandler;
|
||||
readonly permissionLevel: PermissionLevel;
|
||||
}
|
||||
29
ts/src/types/sql.js.d.ts
vendored
Normal file
29
ts/src/types/sql.js.d.ts
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
declare module 'sql.js' {
|
||||
export interface Database {
|
||||
run(sql: string, params?: (string | number | null | Uint8Array)[]): void;
|
||||
exec(sql: string): QueryExecResult[];
|
||||
prepare(sql: string): Statement;
|
||||
export(): Uint8Array;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
export interface Statement {
|
||||
bind(params?: (string | number | null | Uint8Array)[]): boolean;
|
||||
step(): boolean;
|
||||
getAsObject(): Record<string, unknown>;
|
||||
free(): boolean;
|
||||
}
|
||||
|
||||
export interface QueryExecResult {
|
||||
columns: string[];
|
||||
values: (string | number | null | Uint8Array)[][];
|
||||
}
|
||||
|
||||
export interface SqlJsStatic {
|
||||
Database: new (data?: ArrayLike<number>) => Database;
|
||||
}
|
||||
|
||||
export default function initSqlJs(config?: {
|
||||
locateFile?: (file: string) => string;
|
||||
}): Promise<SqlJsStatic>;
|
||||
}
|
||||
193
ts/src/types/waterflow.d.ts
vendored
Normal file
193
ts/src/types/waterflow.d.ts
vendored
Normal file
@ -0,0 +1,193 @@
|
||||
declare module 'waterflow/platform' {
|
||||
import type { Platform } from 'waterflow/platform/types';
|
||||
export function getPlatform(): Platform;
|
||||
export function hasCapability(capability: string): boolean;
|
||||
export function initPlatform(options?: any): Platform;
|
||||
export function resetPlatform(): void;
|
||||
}
|
||||
|
||||
declare module 'waterflow/platform/types' {
|
||||
export interface PlatformAbortSignal {
|
||||
readonly aborted: boolean;
|
||||
readonly reason?: unknown;
|
||||
addEventListener(type: 'abort', listener: () => void): void;
|
||||
removeEventListener(type: 'abort', listener: () => void): void;
|
||||
}
|
||||
|
||||
export interface PlatformAbortController {
|
||||
readonly signal: PlatformAbortSignal;
|
||||
abort(reason?: unknown): void;
|
||||
}
|
||||
|
||||
export interface PathOperations {
|
||||
join(...paths: string[]): string;
|
||||
dirname(p: string): string;
|
||||
basename(p: string, ext?: string): string;
|
||||
extname(p: string): string;
|
||||
normalize(p: string): string;
|
||||
isAbsolute(p: string): boolean;
|
||||
resolve(...paths: string[]): string;
|
||||
relative(from: string, to: string): string;
|
||||
}
|
||||
|
||||
export interface FileReadOptions {
|
||||
encoding?: 'utf-8' | 'binary';
|
||||
start?: number;
|
||||
end?: number;
|
||||
}
|
||||
|
||||
export interface FileWriteOptions {
|
||||
encoding?: 'utf-8' | 'binary';
|
||||
append?: boolean;
|
||||
}
|
||||
|
||||
export interface FileInfo {
|
||||
path: string;
|
||||
name: string;
|
||||
isFile: boolean;
|
||||
isDirectory: boolean;
|
||||
size: number;
|
||||
modifiedTime: number;
|
||||
createdTime: number;
|
||||
}
|
||||
|
||||
export interface FileSystemOperations {
|
||||
readFile(filePath: string, options?: FileReadOptions): Promise<string | ArrayBuffer>;
|
||||
writeFile(filePath: string, data: string | ArrayBuffer, options?: FileWriteOptions): Promise<void>;
|
||||
appendFile(filePath: string, data: string, options?: FileWriteOptions): Promise<void>;
|
||||
deleteFile(filePath: string): Promise<void>;
|
||||
exists(filePath: string): Promise<boolean>;
|
||||
stat(filePath: string): Promise<FileInfo>;
|
||||
readdir(dirPath: string): Promise<FileInfo[]>;
|
||||
mkdir(dirPath: string, recursive?: boolean): Promise<void>;
|
||||
rmdir(dirPath: string, recursive?: boolean): Promise<void>;
|
||||
copy(src: string, dest: string): Promise<void>;
|
||||
move(src: string, dest: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ProcessResult {
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
signal?: string;
|
||||
}
|
||||
|
||||
export interface ProcessOptions {
|
||||
cwd?: string;
|
||||
env?: Record<string, string>;
|
||||
timeout?: number;
|
||||
maxBuffer?: number;
|
||||
}
|
||||
|
||||
export interface ProcessOperations {
|
||||
exec(command: string, options?: ProcessOptions): Promise<ProcessResult>;
|
||||
execFile(file: string, args: string[], options?: ProcessOptions): Promise<ProcessResult>;
|
||||
}
|
||||
|
||||
export interface StorageOperations {
|
||||
get(key: string): Promise<string | null>;
|
||||
set(key: string, value: string, ttl?: number): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
clear(): Promise<void>;
|
||||
keys(): Promise<string[]>;
|
||||
}
|
||||
|
||||
export interface PlatformGlobals {
|
||||
TextEncoder: typeof TextEncoder;
|
||||
TextDecoder: typeof TextDecoder;
|
||||
URL: typeof URL;
|
||||
randomUUID(): string;
|
||||
now(): number;
|
||||
btoa(data: string): string;
|
||||
atob(data: string): string;
|
||||
}
|
||||
|
||||
export interface Logger {
|
||||
info(msg: string, ...args: unknown[]): void;
|
||||
warn(msg: string, ...args: unknown[]): void;
|
||||
error(msg: string, ...args: unknown[]): void;
|
||||
debug(msg: string, ...args: unknown[]): void;
|
||||
}
|
||||
|
||||
export interface Platform {
|
||||
readonly path: PathOperations;
|
||||
readonly fs: FileSystemOperations;
|
||||
readonly process: ProcessOperations;
|
||||
readonly storage: StorageOperations;
|
||||
readonly globals: PlatformGlobals;
|
||||
getInfo(): { runtime: string; os: string; version: string; arch: string; hostname: string };
|
||||
createAbortController(): PlatformAbortController;
|
||||
getEnv(key: string): string | undefined;
|
||||
getAllEnv(): Record<string, string>;
|
||||
setEnv(key: string, value: string): void;
|
||||
getCwd(): string;
|
||||
setCwd(p: string): void;
|
||||
exit(code: number): void;
|
||||
getLogger(): Logger;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'waterflow/runtime/core/tools/tool_interface' {
|
||||
import type { PlatformAbortController, Logger } from 'waterflow/platform/types';
|
||||
|
||||
export type ToolCategory = 'file' | 'code' | 'search' | 'execute' | 'network' | 'analysis' | 'generation' | 'communication' | 'mcp' | 'custom';
|
||||
export type PermissionLevel = 'safe' | 'moderate' | 'dangerous' | 'restricted';
|
||||
export type ToolInput = Record<string, unknown>;
|
||||
export type ToolOutput = string | Record<string, unknown> | void;
|
||||
|
||||
export interface SchemaProperty {
|
||||
type: string;
|
||||
description: string;
|
||||
enum?: string[];
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
pattern?: string;
|
||||
default?: unknown;
|
||||
examples?: unknown[];
|
||||
items?: SchemaProperty;
|
||||
properties?: Record<string, SchemaProperty>;
|
||||
}
|
||||
|
||||
export interface ToolInputSchema {
|
||||
type: 'object';
|
||||
properties: Record<string, SchemaProperty>;
|
||||
required?: string[];
|
||||
additionalProperties?: boolean;
|
||||
}
|
||||
|
||||
export interface ToolOutputSchema {
|
||||
type: 'object';
|
||||
properties: Record<string, SchemaProperty>;
|
||||
format?: 'json' | 'text' | 'markdown' | 'binary';
|
||||
maxSize?: number;
|
||||
maxLines?: number;
|
||||
}
|
||||
|
||||
export interface ToolExecutionContext {
|
||||
toolCallId: string;
|
||||
workingDirectory: string;
|
||||
abortController: PlatformAbortController;
|
||||
config: { timeout?: number };
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export interface Tool {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
readonly category: ToolCategory;
|
||||
readonly inputSchema: ToolInputSchema;
|
||||
readonly outputSchema?: ToolOutputSchema;
|
||||
readonly handler: (params: ToolInput, context: ToolExecutionContext) => Promise<ToolOutput>;
|
||||
readonly permissionLevel: PermissionLevel;
|
||||
readonly alwaysLoad?: boolean;
|
||||
readonly shouldDefer?: boolean;
|
||||
readonly isMcp?: boolean;
|
||||
readonly prompt?: (options: any) => Promise<string>;
|
||||
readonly features?: string[];
|
||||
readonly metadata?: Record<string, unknown>;
|
||||
readonly searchHint?: string;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user