From ae6161059256b8d75122e8c733a92ad033afb33c Mon Sep 17 00:00:00 2001 From: Shanu Date: Fri, 20 Mar 2026 19:39:17 +0530 Subject: [PATCH 1/3] Enhance Neocortex Mastra plugin with new memory management tools - Updated README.md to reflect the addition of new endpoints for saving, recalling, deleting, and managing documents, interactions, and memory synchronization. - Expanded the NeocortexMemoryClient class in client.ts to include methods for syncing memory, recalling thoughts, and managing documents. - Introduced new types and schemas in types.ts to support the new functionalities, ensuring type safety and clarity. - Enhanced the createNeocortexMastraTools function in index.ts to expose the new tools for document management and memory interactions. --- packages/plugin-mastra/README.md | 12 +- packages/plugin-mastra/src/client.ts | 189 +++++++++- packages/plugin-mastra/src/index.ts | 499 ++++++++++++++++++++++++++- packages/plugin-mastra/src/types.ts | 359 +++++++++++++++++++ packages/plugin-mastra/src/utils.ts | 212 ++++++++++++ 5 files changed, 1267 insertions(+), 4 deletions(-) diff --git a/packages/plugin-mastra/README.md b/packages/plugin-mastra/README.md index 96c4498..5ab2361 100644 --- a/packages/plugin-mastra/README.md +++ b/packages/plugin-mastra/README.md @@ -5,7 +5,7 @@ TypeScript plugin for using **Neocortex (Alphahuman) memory** inside Mastra work This package is a small adapter that: - Calls the Alphahuman memory API directly (same contract as `sdk-typescript`) -- Exposes **tools** for saving, recalling, and deleting memory +- Exposes **tools** for saving, recalling, deleting, plus newer endpoints like documents, mirrored query/chat, interactions, sync, recall/thoughts, ingestion jobs, and graph snapshots ## Install @@ -42,7 +42,7 @@ Mastra-native tools (recommended): import { Agent } from "@mastra/core/agent"; import { createNeocortexMastraTools } from "@neocortex/plugin-mastra"; -const { neocortexSaveMemory, neocortexRecallMemory } = createNeocortexMastraTools({ +const { neocortexSaveMemory, neocortexRecallMemory, neocortexDeleteMemory } = createNeocortexMastraTools({ token: process.env.ALPHAHUMAN_API_KEY!, baseUrl: process.env.ALPHAHUMAN_BASE_URL, defaultNamespace: "my-app", @@ -60,6 +60,14 @@ const agent = new Agent({ }); ``` +## Available tools + +`createNeocortexMastraTools()` returns both the original tools (`neocortexSaveMemory`, `neocortexRecallMemory`, `neocortexDeleteMemory`) and additional tools that map to the newer `sdk-typescript` endpoints, including: +- `neocortexInsertDocument`, `neocortexInsertDocumentsBatch`, `neocortexListDocuments`, `neocortexGetDocument`, `neocortexDeleteDocument` +- `neocortexQueryMemoryContext`, `neocortexChatMemoryContext`, `neocortexRecordInteractions`, `neocortexRecallThoughts` +- `neocortexSyncMemory`, `neocortexChatMemory`, `neocortexInteractMemory`, `neocortexRecallMemoryMaster`, `neocortexRecallMemories` +- `neocortexGetIngestionJob`, `neocortexGetGraphSnapshot` + ## Environment variables - `ALPHAHUMAN_API_KEY` (required): Bearer token for the Alphahuman backend diff --git a/packages/plugin-mastra/src/client.ts b/packages/plugin-mastra/src/client.ts index 498090d..e2caae1 100644 --- a/packages/plugin-mastra/src/client.ts +++ b/packages/plugin-mastra/src/client.ts @@ -2,9 +2,31 @@ import type { DeleteMemoryParams, DeleteMemoryResponse, InsertMemoryParams, + InsertDocumentsBatchParams, + InsertDocumentsBatchResponse, + ListDocumentsParams, + ListDocumentsResponse, + GetDocumentParams, + GetDocumentResponse, + DeleteDocumentParams, + DeleteDocumentResponse, NeocortexConfig, QueryMemoryParams, QueryMemoryResponse, + QueryMemoryContextParams, + ChatMemoryParams, + ChatMemoryResponse, + InteractMemoryParams, + InteractMemoryResponse, + RecallThoughtsParams, + RecallThoughtsResponse, + SyncMemoryParams, + SyncMemoryResponse, + RecallMemoryParams, + RecallMemoryResponse, + RecallMemoriesParams, + RecallMemoriesResponse, + GetIngestionJobResponse, } from "./types"; import { resolveBaseUrl } from "./utils"; @@ -57,7 +79,139 @@ export class NeocortexMemoryClient { return this.post("/v1/memory/admin/delete", body); } - private async post(path: string, body: Record): Promise { + // --- Legacy/core endpoints --- + + async syncMemory(params: SyncMemoryParams): Promise { + this.logger?.info?.("Neocortex: syncMemory", { workspaceId: params.workspaceId, agentId: params.agentId }); + const body = { + workspaceId: params.workspaceId, + agentId: params.agentId, + source: params.source, + files: params.files.map((f) => ({ + filePath: f.filePath, + content: f.content, + timestamp: f.timestamp, + hash: f.hash, + })), + }; + return this.post("/v1/memory/sync", body); + } + + async recallMemory(params: RecallMemoryParams = {}): Promise { + return this.post("/v1/memory/recall", { + namespace: params.namespace, + maxChunks: params.maxChunks, + }); + } + + async recallMemories(params: RecallMemoriesParams = {}): Promise { + return this.post("/v1/memory/memories/recall", { + namespace: params.namespace, + topK: params.topK, + minRetention: params.minRetention, + asOf: params.asOf, + }); + } + + async chatMemory(params: ChatMemoryParams): Promise { + return this.post("/v1/memory/chat", { + messages: params.messages, + temperature: params.temperature, + maxTokens: params.maxTokens ?? (params as any).max_tokens, + }); + } + + async interactMemory(params: InteractMemoryParams): Promise { + return this.post("/v1/memory/interact", { + ...params, + }); + } + + // --- Documents & mirrored endpoints --- + + async insertDocument(params: InsertMemoryParams & { documentId?: string }): Promise { + const body = { + title: params.title, + content: params.content, + namespace: params.namespace, + sourceType: params.sourceType ?? "doc", + metadata: params.metadata ?? {}, + priority: (params as any).priority, + createdAt: (params as any).createdAt, + updatedAt: (params as any).updatedAt, + documentId: (params as any).documentId, + }; + return this.post("/v1/memory/documents", body); + } + + async insertDocumentsBatch(params: InsertDocumentsBatchParams): Promise { + return this.post("/v1/memory/documents/batch", params); + } + + async listDocuments(params: ListDocumentsParams = {}): Promise { + return this.get("/v1/memory/documents", params); + } + + async getDocument(params: GetDocumentParams): Promise { + return this.get(`/v1/memory/documents/${encodeURIComponent(params.documentId)}`, { + namespace: params.namespace, + }); + } + + async deleteDocument(params: DeleteDocumentParams): Promise { + // TS SDK uses: DELETE /v1/memory/documents/:documentId?namespace=... + return this.delete(`/v1/memory/documents/${encodeURIComponent(params.documentId)}`, { + namespace: params.namespace, + }); + } + + async queryMemoryContext(params: QueryMemoryContextParams): Promise { + return this.post("/v1/memory/queries", { + query: params.query, + includeReferences: params.includeReferences, + namespace: params.namespace, + maxChunks: params.maxChunks, + documentIds: params.documentIds, + recallOnly: params.recallOnly, + llmQuery: params.llmQuery, + }); + } + + async chatMemoryContext(params: ChatMemoryParams): Promise { + return this.post("/v1/memory/conversations", { + messages: params.messages, + temperature: params.temperature, + maxTokens: params.maxTokens ?? (params as any).max_tokens, + }); + } + + async recordInteractions(params: InteractMemoryParams): Promise { + return this.post("/v1/memory/interactions", params); + } + + async recallThoughts(params: RecallThoughtsParams = {}): Promise { + return this.post("/v1/memory/memories/thoughts", { + namespace: params.namespace, + maxChunks: params.maxChunks ?? (params as any).max_chunks, + temperature: params.temperature, + randomnessSeed: params.randomnessSeed ?? (params as any).randomness_seed, + persist: params.persist, + enablePredictionCheck: + params.enablePredictionCheck ?? (params as any).enable_prediction_check, + thoughtPrompt: params.thoughtPrompt ?? (params as any).thought_prompt, + }); + } + + async getIngestionJob(jobId: string): Promise { + return this.get( + `/v1/memory/ingestion/jobs/${encodeURIComponent(jobId)}`, + undefined, + ); + } + + // --- HTTP helpers --- + + private async post(path: string, body: any): Promise { const url = `${this.baseUrl}${path}`; const res = await fetch(url, { method: "POST", @@ -67,7 +221,40 @@ export class NeocortexMemoryClient { }, body: JSON.stringify(body), }); + return this.parseResponse(res); + } + + private async get( + path: string, + params?: any, + ): Promise { + const qs = params ? new URLSearchParams(Object.entries(params).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)])) : ""; + const url = `${this.baseUrl}${path}${qs ? `?${qs}` : ""}`; + const res = await fetch(url, { + method: "GET", + headers: { + Authorization: `Bearer ${this.token}`, + }, + }); + return this.parseResponse(res); + } + + private async delete( + path: string, + params?: any, + ): Promise { + const qs = params ? new URLSearchParams(Object.entries(params).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)])) : ""; + const url = `${this.baseUrl}${path}${qs ? `?${qs}` : ""}`; + const res = await fetch(url, { + method: "DELETE", + headers: { + Authorization: `Bearer ${this.token}`, + }, + }); + return this.parseResponse(res); + } + private async parseResponse(res: Response): Promise { const text = await res.text(); let json: any; try { diff --git a/packages/plugin-mastra/src/index.ts b/packages/plugin-mastra/src/index.ts index 6342ac9..26fb6dc 100644 --- a/packages/plugin-mastra/src/index.ts +++ b/packages/plugin-mastra/src/index.ts @@ -2,11 +2,26 @@ import { NeocortexMemoryClient } from "./client"; import { createTool } from "@mastra/core/tools"; import { z } from "zod"; import type { + ChatMemoryContextInput, + ChatMemoryInput, DeleteMemoryInput, + DeleteDocumentInput, + GetDocumentInput, + GetIngestionJobInput, + InsertDocumentInput, + InsertDocumentsBatchInput, MastraTool, NeocortexConfig, RecallMemoryInput, + RecallMemoryMasterInput, + RecallMemoriesInput, + RecallThoughtsInput, + InteractMemoryInput, + ListDocumentsInput, + QueryMemoryContextInput, + RecordInteractionsInput, SaveMemoryInput, + SyncMemoryInput, } from "./types"; import { NEOCORTEX_MASTRA_TOOL_SCHEMAS } from "./utils"; @@ -81,7 +96,240 @@ export function createNeocortexMastraTools(config: MastraNeocortexConfig) { execute: async (inputData) => memory.deleteMemory(inputData), }); - return { memory, neocortexSaveMemory, neocortexRecallMemory, neocortexDeleteMemory }; + const neocortexSyncMemory = createTool({ + id: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_sync_memory.name, + description: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_sync_memory.description, + inputSchema: z.object({ + workspace_id: z.string(), + agent_id: z.string(), + source: z.enum(["startup", "agent_end"]).optional(), + files: z.array( + z.object({ + file_path: z.string().optional(), + content: z.string().optional(), + // SDK/types allow string|number timestamps; allow both. + timestamp: z.union([z.string(), z.number()]).optional(), + hash: z.string().optional(), + }) + ), + }), + outputSchema: z.object({ ok: z.literal(true), raw: z.unknown() }), + execute: async (inputData) => memory.syncMemory(inputData as any), + }); + + const neocortexInsertDocument = createTool({ + id: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_insert_document.name, + description: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_insert_document.description, + inputSchema: z.object({ + title: z.string(), + content: z.string(), + namespace: z.string(), + source_type: z.string().optional(), + metadata: z.record(z.unknown()).optional(), + priority: z.string().optional(), + created_at: z.number().optional(), + updated_at: z.number().optional(), + document_id: z.string().optional(), + }), + outputSchema: z.object({ ok: z.literal(true), raw: z.unknown() }), + execute: async (inputData) => memory.insertDocument(inputData), + }); + + const neocortexInsertDocumentsBatch = createTool({ + id: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_insert_documents_batch.name, + description: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_insert_documents_batch.description, + inputSchema: z.object({ + items: z.array( + z.object({ + title: z.string().optional(), + content: z.string().optional(), + namespace: z.string().optional(), + source_type: z.string().optional(), + metadata: z.record(z.unknown()).optional(), + priority: z.string().optional(), + created_at: z.number().optional(), + updated_at: z.number().optional(), + document_id: z.string().optional(), + }) + ), + }), + outputSchema: z.object({ ok: z.literal(true), raw: z.unknown() }), + execute: async (inputData) => memory.insertDocumentsBatch(inputData as any), + }); + + const neocortexListDocuments = createTool({ + id: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_list_documents.name, + description: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_list_documents.description, + inputSchema: z.object({ + namespace: z.string().optional(), + limit: z.number().int().optional(), + offset: z.number().int().optional(), + }), + outputSchema: z.object({ ok: z.literal(true), raw: z.unknown() }), + execute: async (inputData) => memory.listDocuments(inputData), + }); + + const neocortexGetDocument = createTool({ + id: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_get_document.name, + description: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_get_document.description, + inputSchema: z.object({ + document_id: z.string(), + namespace: z.string().optional(), + }), + outputSchema: z.object({ ok: z.literal(true), raw: z.unknown() }), + execute: async (inputData) => memory.getDocument(inputData), + }); + + const neocortexDeleteDocument = createTool({ + id: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_delete_document.name, + description: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_delete_document.description, + inputSchema: z.object({ + document_id: z.string(), + namespace: z.string(), + }), + outputSchema: z.object({ ok: z.literal(true), raw: z.unknown() }), + execute: async (inputData) => memory.deleteDocument(inputData), + }); + + const neocortexQueryMemoryContext = createTool({ + id: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_query_memory_context.name, + description: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_query_memory_context.description, + inputSchema: z.object({ + query: z.string(), + namespace: z.string().optional(), + include_references: z.boolean().optional(), + max_chunks: z.number().int().optional(), + document_ids: z.array(z.string()).optional(), + recall_only: z.boolean().optional(), + llm_query: z.string().optional(), + }), + outputSchema: z.object({ ok: z.literal(true), namespace: z.string(), context: z.string(), raw: z.unknown() }), + execute: async (inputData) => memory.queryMemoryContext(inputData), + }); + + const neocortexChatMemoryContext = createTool({ + id: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_chat_memory_context.name, + description: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_chat_memory_context.description, + inputSchema: z.object({ + messages: z.array(z.object({ role: z.string(), content: z.string() })), + temperature: z.number().optional(), + max_tokens: z.number().int().optional(), + }), + outputSchema: z.object({ ok: z.literal(true), content: z.string(), raw: z.unknown() }), + execute: async (inputData) => memory.chatMemoryContext(inputData), + }); + + const neocortexRecordInteractions = createTool({ + id: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_record_interactions.name, + description: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_record_interactions.description, + inputSchema: z.object({ + namespace: z.string(), + entity_names: z.array(z.string()), + description: z.string().optional(), + interaction_level: z.string().optional(), + interaction_levels: z.array(z.string()).optional(), + timestamp: z.number().optional(), + }), + outputSchema: z.object({ ok: z.literal(true), raw: z.unknown() }), + execute: async (inputData) => memory.recordInteractions(inputData), + }); + + const neocortexRecallThoughts = createTool({ + id: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_recall_thoughts.name, + description: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_recall_thoughts.description, + inputSchema: z.object({ + namespace: z.string().optional(), + max_chunks: z.number().int().optional(), + temperature: z.number().optional(), + randomness_seed: z.number().int().optional(), + persist: z.boolean().optional(), + enable_prediction_check: z.boolean().optional(), + thought_prompt: z.string().optional(), + }), + outputSchema: z.object({ ok: z.literal(true), thought: z.string().optional(), raw: z.unknown() }), + execute: async (inputData) => memory.recallThoughts(inputData), + }); + + const neocortexChatMemory = createTool({ + id: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_chat_memory.name, + description: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_chat_memory.description, + inputSchema: z.object({ + messages: z.array(z.object({ role: z.string(), content: z.string() })), + temperature: z.number().optional(), + max_tokens: z.number().int().optional(), + }), + outputSchema: z.object({ ok: z.literal(true), content: z.string(), raw: z.unknown() }), + execute: async (inputData) => memory.chatMemory(inputData), + }); + + const neocortexInteractMemory = createTool({ + id: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_interact_memory.name, + description: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_interact_memory.description, + inputSchema: z.object({ + namespace: z.string(), + entity_names: z.array(z.string()), + description: z.string().optional(), + interaction_level: z.string().optional(), + interaction_levels: z.array(z.string()).optional(), + timestamp: z.number().optional(), + }), + outputSchema: z.object({ ok: z.literal(true), raw: z.unknown() }), + execute: async (inputData) => memory.interactMemory(inputData), + }); + + const neocortexRecallMemoryMaster = createTool({ + id: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_recall_memory_master.name, + description: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_recall_memory_master.description, + inputSchema: z.object({ + namespace: z.string().optional(), + max_chunks: z.number().int().optional(), + }), + outputSchema: z.object({ ok: z.literal(true), namespace: z.string(), context: z.string(), raw: z.unknown() }), + execute: async (inputData) => memory.recallMemoryMaster(inputData), + }); + + const neocortexRecallMemories = createTool({ + id: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_recall_memories.name, + description: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_recall_memories.description, + inputSchema: z.object({ + namespace: z.string().optional(), + top_k: z.number().optional(), + min_retention: z.number().optional(), + as_of: z.number().optional(), + }), + outputSchema: z.object({ ok: z.literal(true), raw: z.unknown() }), + execute: async (inputData) => memory.recallMemories(inputData), + }); + + const neocortexGetIngestionJob = createTool({ + id: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_get_ingestion_job.name, + description: NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_get_ingestion_job.description, + inputSchema: z.object({ job_id: z.string() }), + outputSchema: z.object({ ok: z.literal(true), raw: z.unknown() }), + execute: async (inputData) => memory.getIngestionJob(inputData), + }); + + return { + memory, + neocortexSaveMemory, + neocortexRecallMemory, + neocortexDeleteMemory, + neocortexSyncMemory, + neocortexInsertDocument, + neocortexInsertDocumentsBatch, + neocortexListDocuments, + neocortexGetDocument, + neocortexDeleteDocument, + neocortexQueryMemoryContext, + neocortexChatMemoryContext, + neocortexRecordInteractions, + neocortexRecallThoughts, + neocortexChatMemory, + neocortexInteractMemory, + neocortexRecallMemoryMaster, + neocortexRecallMemories, + neocortexGetIngestionJob, + }; } /** @@ -159,6 +407,195 @@ export class MastraNeocortexMemory { }; } + private extractContext(data: any): string { + const llmMsg = data?.llmContextMessage || data?.response; + if (typeof llmMsg === "string" && llmMsg.trim()) return llmMsg.trim(); + + const chunks = data?.context?.chunks ?? []; + if (!Array.isArray(chunks) || chunks.length === 0) return "No relevant memories found."; + + const texts: string[] = []; + for (const chunk of chunks) { + if (!chunk || typeof chunk !== "object") continue; + const text = (chunk as any).content ?? (chunk as any).text ?? (chunk as any).body ?? ""; + if (typeof text === "string" && text.trim()) texts.push(text.trim()); + } + + return texts.length ? texts.join("\n\n") : "No relevant memories found."; + } + + async syncMemory(input: SyncMemoryInput): Promise<{ ok: true; raw: unknown }> { + const res = await this.client.syncMemory({ + workspaceId: input.workspace_id, + agentId: input.agent_id, + source: input.source, + files: (input.files ?? []).map((f: any) => ({ + filePath: f.file_path ?? f.filePath, + content: f.content, + timestamp: String(f.timestamp ?? ""), + hash: f.hash, + })), + }); + return { ok: true, raw: res }; + } + + async insertDocument(input: InsertDocumentInput): Promise<{ ok: true; raw: unknown }> { + const res = await this.client.insertDocument({ + title: input.title, + content: input.content, + namespace: input.namespace, + sourceType: input.source_type ?? "doc", + metadata: input.metadata ?? {}, + priority: input.priority, + createdAt: input.created_at, + updatedAt: input.updated_at, + documentId: input.document_id, + } as any); + return { ok: true, raw: res }; + } + + async insertDocumentsBatch(input: InsertDocumentsBatchInput): Promise<{ ok: true; raw: unknown }> { + const items = (input.items ?? []).map((it: any) => ({ + title: it.title, + content: it.content, + namespace: it.namespace, + sourceType: it.source_type ?? it.sourceType, + metadata: it.metadata ?? {}, + priority: it.priority, + createdAt: it.created_at ?? it.createdAt, + updatedAt: it.updated_at ?? it.updatedAt, + documentId: it.document_id ?? it.documentId, + })); + const res = await this.client.insertDocumentsBatch({ items }); + return { ok: true, raw: res }; + } + + async listDocuments(input: ListDocumentsInput): Promise<{ ok: true; raw: unknown }> { + const res = await this.client.listDocuments({ + namespace: input.namespace, + limit: input.limit, + offset: input.offset, + }); + return { ok: true, raw: res }; + } + + async getDocument(input: GetDocumentInput): Promise<{ ok: true; raw: unknown }> { + const res = await this.client.getDocument({ + documentId: input.document_id, + namespace: input.namespace, + }); + return { ok: true, raw: res }; + } + + async deleteDocument(input: DeleteDocumentInput): Promise<{ ok: true; raw: unknown }> { + const res = await this.client.deleteDocument({ + documentId: input.document_id, + namespace: input.namespace, + }); + return { ok: true, raw: res }; + } + + async queryMemoryContext( + input: QueryMemoryContextInput, + ): Promise<{ ok: true; namespace: string; context: string; raw: unknown }> { + const namespace = input.namespace?.trim() || this.defaultNamespace; + const res = await this.client.queryMemoryContext({ + query: input.query, + includeReferences: input.include_references, + namespace, + maxChunks: input.max_chunks, + documentIds: input.document_ids, + recallOnly: input.recall_only, + llmQuery: input.llm_query, + }); + const data = (res as any)?.data ?? {}; + return { ok: true, namespace, context: this.extractContext(data), raw: data }; + } + + async chatMemoryContext( + input: ChatMemoryContextInput, + ): Promise<{ ok: true; content: string; raw: unknown }> { + const res = await this.client.chatMemoryContext({ + messages: input.messages, + temperature: input.temperature, + maxTokens: input.max_tokens, + } as any); + const content = (res as any)?.data?.content; + return { ok: true, content: typeof content === "string" ? content : "", raw: res }; + } + + async recordInteractions(input: RecordInteractionsInput): Promise<{ ok: true; raw: unknown }> { + const res = await this.client.recordInteractions({ + namespace: input.namespace, + entityNames: input.entity_names, + description: input.description, + interactionLevel: input.interaction_level, + interactionLevels: input.interaction_levels, + timestamp: input.timestamp, + } as any); + return { ok: true, raw: res }; + } + + async recallThoughts(input: RecallThoughtsInput): Promise<{ ok: true; thought?: string; raw: unknown }> { + const res = await this.client.recallThoughts({ + namespace: input.namespace, + maxChunks: input.max_chunks, + temperature: input.temperature, + randomnessSeed: input.randomness_seed, + persist: input.persist, + enablePredictionCheck: input.enable_prediction_check, + thoughtPrompt: input.thought_prompt, + } as any); + const thought = (res as any)?.data?.thought; + return { ok: true, thought: typeof thought === "string" ? thought : undefined, raw: res }; + } + + async chatMemory(input: ChatMemoryInput): Promise<{ ok: true; content: string; raw: unknown }> { + const res = await this.client.chatMemory({ + messages: input.messages, + temperature: input.temperature, + maxTokens: input.max_tokens, + } as any); + const content = (res as any)?.data?.content; + return { ok: true, content: typeof content === "string" ? content : "", raw: res }; + } + + async interactMemory(input: InteractMemoryInput): Promise<{ ok: true; raw: unknown }> { + const res = await this.client.interactMemory({ + namespace: input.namespace, + entityNames: input.entity_names, + description: input.description, + interactionLevel: input.interaction_level, + interactionLevels: input.interaction_levels, + timestamp: input.timestamp, + } as any); + return { ok: true, raw: res }; + } + + async recallMemoryMaster( + input: RecallMemoryMasterInput, + ): Promise<{ ok: true; namespace: string; context: string; raw: unknown }> { + const namespace = input.namespace?.trim() || this.defaultNamespace; + const res = await this.client.recallMemory({ namespace, maxChunks: input.max_chunks }); + const data = (res as any)?.data ?? {}; + return { ok: true, namespace, context: this.extractContext(data), raw: data }; + } + + async recallMemories(input: RecallMemoriesInput): Promise<{ ok: true; raw: unknown }> { + const res = await this.client.recallMemories({ + namespace: input.namespace, + topK: input.top_k, + minRetention: input.min_retention, + asOf: input.as_of, + }); + return { ok: true, raw: res }; + } + + async getIngestionJob(input: GetIngestionJobInput): Promise<{ ok: true; raw: unknown }> { + const res = await this.client.getIngestionJob(input.job_id); + return { ok: true, raw: res }; + } + /** * Tools you can register with Mastra. * @@ -178,6 +615,66 @@ export class MastraNeocortexMemory { ...NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_delete_memory, execute: (params: DeleteMemoryInput) => this.deleteMemory(params), }, + { + ...NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_sync_memory, + execute: (params: SyncMemoryInput) => this.syncMemory(params), + }, + { + ...NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_insert_document, + execute: (params: InsertDocumentInput) => this.insertDocument(params), + }, + { + ...NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_insert_documents_batch, + execute: (params: InsertDocumentsBatchInput) => this.insertDocumentsBatch(params), + }, + { + ...NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_list_documents, + execute: (params: ListDocumentsInput) => this.listDocuments(params), + }, + { + ...NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_get_document, + execute: (params: GetDocumentInput) => this.getDocument(params), + }, + { + ...NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_delete_document, + execute: (params: DeleteDocumentInput) => this.deleteDocument(params), + }, + { + ...NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_query_memory_context, + execute: (params: QueryMemoryContextInput) => this.queryMemoryContext(params), + }, + { + ...NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_chat_memory_context, + execute: (params: ChatMemoryContextInput) => this.chatMemoryContext(params), + }, + { + ...NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_record_interactions, + execute: (params: RecordInteractionsInput) => this.recordInteractions(params), + }, + { + ...NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_recall_thoughts, + execute: (params: RecallThoughtsInput) => this.recallThoughts(params), + }, + { + ...NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_chat_memory, + execute: (params: ChatMemoryInput) => this.chatMemory(params), + }, + { + ...NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_interact_memory, + execute: (params: InteractMemoryInput) => this.interactMemory(params), + }, + { + ...NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_recall_memory_master, + execute: (params: RecallMemoryMasterInput) => this.recallMemoryMaster(params), + }, + { + ...NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_recall_memories, + execute: (params: RecallMemoriesInput) => this.recallMemories(params), + }, + { + ...NEOCORTEX_MASTRA_TOOL_SCHEMAS.neocortex_get_ingestion_job, + execute: (params: GetIngestionJobInput) => this.getIngestionJob(params), + }, ]; } } diff --git a/packages/plugin-mastra/src/types.ts b/packages/plugin-mastra/src/types.ts index a1f7939..7cb24f6 100644 --- a/packages/plugin-mastra/src/types.ts +++ b/packages/plugin-mastra/src/types.ts @@ -69,6 +69,258 @@ export interface DeleteMemoryResponse { }; } +// ---------------------------- +// Newer endpoints (aligned with sdk-typescript) +// ---------------------------- + +export interface SyncFileParams { + filePath: string; + content: string; + timestamp: string; + hash: string; +} + +export interface SyncMemoryParams { + workspaceId: string; + agentId: string; + source?: "startup" | "agent_end"; + files: SyncFileParams[]; +} + +export interface SyncMemoryResponse { + success: boolean; + data: { + synced?: number; + jobId?: string; + state?: string; + }; +} + +export interface ChatMessage { + role: string; + content: string; +} + +export interface ChatMemoryParams { + messages: ChatMessage[]; + temperature?: number; + maxTokens?: number; + max_tokens?: number; +} + +export interface ChatMemoryResponse { + success: boolean; + data: { + content?: string; + usage?: { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + }; + model?: string; + jobId?: string; + state?: string; + }; +} + +export interface InteractMemoryParams { + namespace: string; + entityNames: string[]; + description?: string; + interactionLevel?: "view" | "read" | "react" | "engage" | "create"; + interactionLevels?: Array<"view" | "read" | "react" | "engage" | "create">; + timestamp?: number; +} + +export interface InteractMemoryResponse { + success: boolean; + data: { + status?: string; + interactionsRecorded?: number; + entityNames?: string[]; + timestampUsed?: number; + jobId?: string; + state?: string; + }; +} + +export interface RecallThoughtsParams { + namespace?: string; + maxChunks?: number; + max_chunks?: number; + temperature?: number; + randomnessSeed?: number; + randomness_seed?: number; + persist?: boolean; + enablePredictionCheck?: boolean; + enable_prediction_check?: boolean; + thoughtPrompt?: string; + thought_prompt?: string; +} + +export interface RecallThoughtsResponse { + success: boolean; + data: { + thought?: string; + context?: Record; + llm_context_message?: string; + usage?: { + llm_input_tokens: number; + llm_output_tokens: number; + embedding_tokens: number; + cost_usd: number; + }; + cached?: boolean; + latency_seconds?: number; + persisted?: boolean; + jobId?: string; + state?: string; + }; +} + +export interface GetIngestionJobResponse { + success: boolean; + data: { + jobId: string; + state: string; + endpoint: string; + attempts: number; + error: string | null; + response: Record | null; + createdAt: string; + startedAt: string | null; + completedAt: string | null; + }; +} + +export interface InsertDocumentsBatchParams { + items: Array<{ + title: string; + content: string; + namespace: string; + sourceType?: "doc" | "chat" | "email"; + metadata?: Record; + priority?: "high" | "medium" | "low"; + createdAt?: number; + updatedAt?: number; + documentId?: string; + }>; +} + +export interface InsertDocumentsBatchResponse { + success: boolean; + data: { + status?: string; + results?: any[]; + state?: string; + accepted?: Array<{ index: number; jobId: string }>; + }; +} + +export interface ListDocumentsParams { + namespace?: string; + limit?: number; + offset?: number; +} + +export interface ListDocumentsResponse { + success: boolean; + data: Record; +} + +export interface GetDocumentParams { + documentId: string; + namespace?: string; +} + +export interface GetDocumentResponse { + success: boolean; + data: Record; +} + +export interface DeleteDocumentParams { + documentId: string; + namespace: string; +} + +export interface DeleteDocumentResponse { + success: boolean; + data: { + status?: string; + message?: string; + nodesDeleted?: number; + jobId?: string; + state?: string; + [key: string]: unknown; + }; +} + +export interface GetGraphSnapshotParams { + namespace?: string; + mode?: "master" | "latest_chunks"; + limit?: number; + seed_limit?: number; +} + +export interface GetGraphSnapshotResponse { + success: boolean; + data: Record; +} + +export interface QueryMemoryContextParams { + query: string; + includeReferences?: boolean; + namespace?: string; + maxChunks?: number; + documentIds?: string[]; + recallOnly?: boolean; + llmQuery?: string; +} + +export interface RecallMemoryParams { + namespace?: string; + maxChunks?: number; +} + +export interface RecallMemoryResponse { + success: boolean; + data: QueryMemoryResponseData & { + latencySeconds?: number; + counts?: { + numEntities: number; + numRelations: number; + numChunks: number; + }; + }; +} + +export interface RecallMemoriesParams { + namespace?: string; + topK?: number; + minRetention?: number; + asOf?: number; +} + +export interface MemoryItemRecalled { + type: string; + id: string; + content: string; + score: number; + retention: number; + last_accessed_at?: string; + access_count: number; + stability_days: number; +} + +export interface RecallMemoriesResponse { + success: boolean; + data: { + memories?: MemoryItemRecalled[]; + jobId?: string; + state?: string; + }; +} + /** A minimal “tool” interface compatible with most TS agent frameworks. */ export interface MastraTool { name: string; @@ -94,3 +346,110 @@ export interface DeleteMemoryInput { namespace?: string; } +// Tool input shapes (snake_case) for the Mastra tool schemas. +export interface SyncMemoryInput { + workspace_id: string; + agent_id: string; + source?: "startup" | "agent_end"; + files: Array<{ + file_path: string; + content: string; + timestamp: string | number; + hash: string; + }>; +} + +export interface InsertDocumentInput { + title: string; + content: string; + namespace: string; + source_type?: string; + metadata?: Record; + priority?: string; + created_at?: number; + updated_at?: number; + document_id?: string; +} + +export interface InsertDocumentsBatchInput { + items: InsertDocumentInput[]; +} + +export interface ListDocumentsInput { + namespace?: string; + limit?: number; + offset?: number; +} + +export interface GetDocumentInput { + document_id: string; + namespace?: string; +} + +export interface DeleteDocumentInput { + document_id: string; + namespace: string; +} + +export interface QueryMemoryContextInput { + query: string; + namespace?: string; + include_references?: boolean; + max_chunks?: number; + document_ids?: string[]; + recall_only?: boolean; + llm_query?: string; +} + +export interface ChatMemoryContextInput { + messages: Array<{ role: string; content: string }>; + temperature?: number; + max_tokens?: number; +} + +export interface RecordInteractionsInput { + namespace: string; + entity_names: string[]; + description?: string; + interaction_level?: string; + interaction_levels?: string[]; + timestamp?: number; +} + +export interface RecallThoughtsInput { + namespace?: string; + max_chunks?: number; + temperature?: number; + randomness_seed?: number; + persist?: boolean; + enable_prediction_check?: boolean; + thought_prompt?: string; +} + +export interface ChatMemoryInput extends ChatMemoryContextInput {} + +export interface InteractMemoryInput extends RecordInteractionsInput {} + +export interface RecallMemoryMasterInput { + namespace?: string; + max_chunks?: number; +} + +export interface RecallMemoriesInput { + namespace?: string; + top_k?: number; + min_retention?: number; + as_of?: number; +} + +export interface GetIngestionJobInput { + job_id: string; +} + +export interface GetGraphSnapshotInput { + namespace?: string; + mode?: string; + limit?: number; + seed_limit?: number; +} + diff --git a/packages/plugin-mastra/src/utils.ts b/packages/plugin-mastra/src/utils.ts index 7b87131..0b2a2d6 100644 --- a/packages/plugin-mastra/src/utils.ts +++ b/packages/plugin-mastra/src/utils.ts @@ -67,5 +67,217 @@ export const NEOCORTEX_MASTRA_TOOL_SCHEMAS = { required: [], }), }, + neocortex_sync_memory: { + name: "neocortex_sync_memory", + description: "Sync OpenClaw memory files (POST /v1/memory/sync).", + parameters: objectSchema({ + properties: { + workspace_id: { type: "string", description: "Workspace identifier." }, + agent_id: { type: "string", description: "Agent identifier." }, + source: { type: "string", description: "Optional source: startup | agent_end." }, + files: { + type: "array", + description: "Files to sync.", + items: { type: "object" }, + }, + }, + required: ["workspace_id", "agent_id", "files"], + }), + }, + neocortex_insert_document: { + name: "neocortex_insert_document", + description: "Insert a single memory document (POST /v1/memory/documents).", + parameters: objectSchema({ + properties: { + title: { type: "string", description: "Document title." }, + content: { type: "string", description: "Document content." }, + namespace: { type: "string", description: "Namespace." }, + source_type: { type: "string", description: "Optional sourceType: doc | chat | email." }, + metadata: { type: "object", description: "Optional metadata." }, + priority: { type: "string", description: "Optional priority: high | medium | low." }, + created_at: { type: "number", description: "Optional Unix timestamp (seconds)." }, + updated_at: { type: "number", description: "Optional Unix timestamp (seconds)." }, + document_id: { type: "string", description: "Optional documentId override." }, + }, + required: ["title", "content", "namespace"], + }), + }, + neocortex_insert_documents_batch: { + name: "neocortex_insert_documents_batch", + description: "Insert multiple memory documents (POST /v1/memory/documents/batch).", + parameters: objectSchema({ + properties: { + items: { type: "array", description: "Document items.", items: { type: "object" } }, + }, + required: ["items"], + }), + }, + neocortex_list_documents: { + name: "neocortex_list_documents", + description: "List ingested documents (GET /v1/memory/documents).", + parameters: objectSchema({ + properties: { + namespace: { type: "string", description: "Optional namespace." }, + limit: { type: "integer", description: "Optional page size." }, + offset: { type: "integer", description: "Optional page offset." }, + }, + required: [], + }), + }, + neocortex_get_document: { + name: "neocortex_get_document", + description: "Get a memory document (GET /v1/memory/documents/:documentId).", + parameters: objectSchema({ + properties: { + document_id: { type: "string", description: "Document id." }, + namespace: { type: "string", description: "Optional namespace." }, + }, + required: ["document_id"], + }), + }, + neocortex_delete_document: { + name: "neocortex_delete_document", + description: "Delete a memory document (DELETE /v1/memory/documents/:documentId?namespace=...).", + parameters: objectSchema({ + properties: { + document_id: { type: "string", description: "Document id." }, + namespace: { type: "string", description: "Namespace." }, + }, + required: ["document_id", "namespace"], + }), + }, + neocortex_query_memory_context: { + name: "neocortex_query_memory_context", + description: "Query memory context (POST /v1/memory/queries).", + parameters: objectSchema({ + properties: { + query: { type: "string", description: "Query string." }, + namespace: { type: "string", description: "Optional namespace." }, + include_references: { type: "boolean", description: "Include references." }, + max_chunks: { type: "integer", description: "Optional chunk limit." }, + document_ids: { type: "array", description: "Optional document filters.", items: { type: "string" } }, + recall_only: { type: "boolean", description: "Recall-only mode." }, + llm_query: { type: "string", description: "Optional LLM query override." }, + }, + required: ["query"], + }), + }, + neocortex_chat_memory_context: { + name: "neocortex_chat_memory_context", + description: "Chat with memory context (POST /v1/memory/conversations).", + parameters: objectSchema({ + properties: { + messages: { type: "array", description: "Messages (role/content).", items: { type: "object" } }, + temperature: { type: "number", description: "Optional temperature." }, + max_tokens: { type: "integer", description: "Optional max tokens." }, + }, + required: ["messages"], + }), + }, + neocortex_record_interactions: { + name: "neocortex_record_interactions", + description: "Record interactions (POST /v1/memory/interactions).", + parameters: objectSchema({ + properties: { + namespace: { type: "string", description: "Namespace." }, + entity_names: { type: "array", description: "Entity names.", items: { type: "string" } }, + description: { type: "string", description: "Optional description." }, + interaction_level: { type: "string", description: "Optional interaction level." }, + interaction_levels: { type: "array", description: "Optional multiple interaction levels.", items: { type: "string" } }, + timestamp: { type: "number", description: "Optional timestamp." }, + }, + required: ["namespace", "entity_names"], + }), + }, + neocortex_recall_thoughts: { + name: "neocortex_recall_thoughts", + description: "Generate reflective thoughts (POST /v1/memory/memories/thoughts).", + parameters: objectSchema({ + properties: { + namespace: { type: "string", description: "Optional namespace." }, + max_chunks: { type: "integer", description: "Optional max chunks." }, + temperature: { type: "number", description: "Optional temperature." }, + randomness_seed: { type: "integer", description: "Optional randomness seed." }, + persist: { type: "boolean", description: "Optional persist." }, + enable_prediction_check: { type: "boolean", description: "Optional enablePredictionCheck." }, + thought_prompt: { type: "string", description: "Optional thoughtPrompt." }, + }, + required: [], + }), + }, + neocortex_chat_memory: { + name: "neocortex_chat_memory", + description: "Chat with memory cache (POST /v1/memory/chat).", + parameters: objectSchema({ + properties: { + messages: { type: "array", description: "Messages (role/content).", items: { type: "object" } }, + temperature: { type: "number", description: "Optional temperature." }, + max_tokens: { type: "integer", description: "Optional max tokens." }, + }, + required: ["messages"], + }), + }, + neocortex_interact_memory: { + name: "neocortex_interact_memory", + description: "Record interactions (core) (POST /v1/memory/interact).", + parameters: objectSchema({ + properties: { + namespace: { type: "string", description: "Namespace." }, + entity_names: { type: "array", description: "Entity names.", items: { type: "string" } }, + description: { type: "string", description: "Optional description." }, + interaction_level: { type: "string", description: "Optional interaction level." }, + interaction_levels: { type: "array", description: "Optional multiple interaction levels.", items: { type: "string" } }, + timestamp: { type: "number", description: "Optional timestamp." }, + }, + required: ["namespace", "entity_names"], + }), + }, + neocortex_recall_memory_master: { + name: "neocortex_recall_memory_master", + description: "Recall context from master node (POST /v1/memory/recall).", + parameters: objectSchema({ + properties: { + namespace: { type: "string", description: "Optional namespace." }, + max_chunks: { type: "integer", description: "Optional max chunks." }, + }, + required: [], + }), + }, + neocortex_recall_memories: { + name: "neocortex_recall_memories", + description: "Recall memories from Ebbinghaus bank (POST /v1/memory/memories/recall).", + parameters: objectSchema({ + properties: { + namespace: { type: "string", description: "Optional namespace." }, + top_k: { type: "number", description: "Optional topK." }, + min_retention: { type: "number", description: "Optional minRetention." }, + as_of: { type: "number", description: "Optional asOf timestamp." }, + }, + required: [], + }), + }, + neocortex_get_ingestion_job: { + name: "neocortex_get_ingestion_job", + description: "Get ingestion job status (GET /v1/memory/ingestion/jobs/:jobId).", + parameters: objectSchema({ + properties: { + job_id: { type: "string", description: "Job id." }, + }, + required: ["job_id"], + }), + }, + neocortex_get_graph_snapshot: { + name: "neocortex_get_graph_snapshot", + description: "Get admin graph snapshot (GET /v1/memory/admin/graph-snapshot).", + parameters: objectSchema({ + properties: { + namespace: { type: "string", description: "Optional namespace." }, + mode: { type: "string", description: "Optional mode (master|latest_chunks)." }, + limit: { type: "integer", description: "Optional limit." }, + seed_limit: { type: "integer", description: "Optional seed limit." }, + }, + required: [], + }), + }, } as const satisfies Record>; From 6aae0a5596b12ebda38bbc6334a04a0aa237c23d Mon Sep 17 00:00:00 2001 From: Shanu Date: Fri, 20 Mar 2026 19:44:32 +0530 Subject: [PATCH 2/3] Add new tools for the new endpoints and enhance example.py and README.md for Neocortex plugin - Added a .gitignore file to exclude unnecessary files and directories from version control. - Updated example.py to include detailed instructions for saving and recalling memories, as well as managing document workflows. - Enhanced README.md to clarify the available tools and their functionalities, including new document management features and memory interaction methods. --- packages/plugin-agno/.gitignore | 23 + packages/plugin-agno/README.md | 19 +- packages/plugin-agno/example.py | 34 +- packages/plugin-agno/neocortex_agno/tools.py | 779 +++++++++++++++++++ 4 files changed, 850 insertions(+), 5 deletions(-) create mode 100644 packages/plugin-agno/.gitignore diff --git a/packages/plugin-agno/.gitignore b/packages/plugin-agno/.gitignore new file mode 100644 index 0000000..5c65f70 --- /dev/null +++ b/packages/plugin-agno/.gitignore @@ -0,0 +1,23 @@ +node_modules/ +dist/ +package-lock.json +npm-debug.log* +yarn.lock +.pnpm-debug.log* + +*.tsbuildinfo + +.venv/ +__pycache__/ +*.py[cod] +*.pyo +*.pyd +*.egg-info/ +.eggs/ + +.DS_Store +.vscode/ +.idea/ +*.swp +*.swo + diff --git a/packages/plugin-agno/README.md b/packages/plugin-agno/README.md index 8438998..b051878 100644 --- a/packages/plugin-agno/README.md +++ b/packages/plugin-agno/README.md @@ -41,7 +41,7 @@ agent.print_response("What theme do I prefer?", stream=True) ## Available tools -The `NeocortexTools` toolkit exposes three tools to the agent: +The `NeocortexTools` toolkit exposes tools aligned with the TinyHumans/Neocortex SDK endpoints: | Tool | Description | @@ -49,6 +49,21 @@ The `NeocortexTools` toolkit exposes three tools to the agent: | `save_memory` | Save or update a memory (key, content, namespace, optional metadata). | | `recall_memory` | Recall relevant memories for a natural-language query in a namespace. | | `delete_memory` | Delete one or more memories by key/keys or delete all in a namespace. | +| `sync_memory` | Sync OpenClaw memory files (workspace/agent + file objects). | +| `insert_document` | Insert a single memory document (title/content/namespace). | +| `insert_documents_batch` | Insert multiple documents in one call. | +| `list_documents` | List documents in a namespace. | +| `get_document` | Get a specific document by `document_id`. | +| `delete_document` | Delete a specific document by `document_id`. | +| `query_memory_context` | Query mirrored memory context (`/v1/memory/queries`). | +| `chat_memory_context` | Chat with memory context (`/v1/memory/conversations`). | +| `record_interactions` | Record interaction signals (`/v1/memory/interactions`). | +| `recall_thoughts` | Generate reflective thoughts (`/v1/memory/memories/thoughts`). | +| `chat_memory` | Chat with memory cache (`/v1/memory/chat`). | +| `interact_memory` | Record entity interactions (`/v1/memory/interact`). | +| `recall_memory_master` | Recall context from the master node (`/v1/memory/recall`). | +| `recall_memories` | Recall memories from the Ebbinghaus bank (`/v1/memory/memories/recall`). | +| `get_ingestion_job` | Check ingestion job status (`/v1/memory/ingestion/jobs/:jobId`). | Credentials (`token`, `model_id`, `base_url`) are set when constructing `NeocortexTools` and are **never** passed as tool arguments, so the LLM cannot see or override them. @@ -60,7 +75,7 @@ Credentials (`token`, `model_id`, `base_url`) are set when constructing `Neocort ## Error handling -On API failures, the underlying client raises `TinyHumanError`. You can catch it for logging or user-facing messages: +On API failures, the underlying client raises `AlphahumanError`. You can catch it for logging or user-facing messages: ```python from neocortex_agno import NeocortexTools, AlphahumanError diff --git a/packages/plugin-agno/example.py b/packages/plugin-agno/example.py index 28559a8..845f8b7 100644 --- a/packages/plugin-agno/example.py +++ b/packages/plugin-agno/example.py @@ -5,6 +5,11 @@ export ALPHAHUMAN_BASE_URL="" export OPENAI_API_KEY="" python example.py + +This example demonstrates both: +- Saving/recalling simple memories (preferences) +- Document + context workflows (insert/list/get documents, query/chat context, + record interactions, and recall thoughts) """ import os @@ -29,9 +34,16 @@ def main() -> None: ) ], instructions=( - "Use the memory tools to remember and recall user preferences and context. " - "When the user tells you something to remember, use save_memory. " - "When answering questions that might use stored context, use recall_memory first." + "Use the memory tools to remember and recall user preferences and context.\n" + "When the user tells you something to remember, use save_memory.\n" + "When answering questions that might use stored context, use recall_memory first.\n" + "If the user asks about documents or document-backed context, use:\n" + "- insert_document / insert_documents_batch\n" + "- list_documents / get_document\n" + "- query_memory_context (POST /v1/memory/queries)\n" + "- chat_memory_context (POST /v1/memory/conversations)\n" + "If the user asks to track signal-level memory, use record_interactions.\n" + "If the user asks for reflective/summary insights from memory, use recall_thoughts." ), markdown=True, ) @@ -47,6 +59,22 @@ def main() -> None: ) agent.print_response("What theme do I prefer?", stream=True) + print() + print("Document + context workflow:") + agent.print_response( + "Create a document in namespace 'agno-docs' titled 'Alex Preferences'. " + "Store the content: 'Alex prefers dark mode and wants succinct answers.'. " + "Next, query_memory_context in 'agno-docs' for: 'What does Alex prefer?' " + "and use that output to answer. " + "Then call chat_memory_context with messages=[{'role':'user','content':'What does Alex prefer?'}] " + "using the same namespace 'agno-docs' context. " + "After that, call record_interactions in 'agno-docs' with " + "entity_names=['ENTITY-AGNO-A','ENTITY-AGNO-B'] and interaction_level='engage'. " + "Finally, call recall_thoughts for 'agno-docs' with max_chunks=5. " + "Return a short summary of each step's outcome.", + stream=True, + ) + if __name__ == "__main__": main() diff --git a/packages/plugin-agno/neocortex_agno/tools.py b/packages/plugin-agno/neocortex_agno/tools.py index 7465df2..81fbdab 100644 --- a/packages/plugin-agno/neocortex_agno/tools.py +++ b/packages/plugin-agno/neocortex_agno/tools.py @@ -2,8 +2,11 @@ from __future__ import annotations +import json import os +import time from typing import Any, Optional, Sequence +from urllib.parse import quote import httpx from agno.tools import Toolkit @@ -31,6 +34,21 @@ class AlphahumanMemoryClient: - POST /v1/memory/insert - POST /v1/memory/query - POST /v1/memory/admin/delete + - POST /v1/memory/sync + - POST /v1/memory/recall + - POST /v1/memory/memories/recall + - POST /v1/memory/memories/thoughts + - POST /v1/memory/chat + - POST /v1/memory/interact + - POST /v1/memory/interactions + - POST /v1/memory/queries + - POST /v1/memory/conversations + - GET /v1/memory/ingestion/jobs/:jobId + - POST /v1/memory/documents + - POST /v1/memory/documents/batch + - GET /v1/memory/documents + - GET /v1/memory/documents/:documentId + - DELETE /v1/memory/documents/:documentId """ def __init__(self, token: str, base_url: Optional[str] = None) -> None: @@ -87,8 +105,407 @@ def delete_memory(self, *, namespace: Optional[str] = None) -> dict[str, Any]: body: dict[str, Any] = {"namespace": namespace} return self._post("/v1/memory/admin/delete", body) + def insert_document( + self, + *, + title: str, + content: str, + namespace: str, + source_type: Optional[str] = None, + metadata: Optional[dict[str, Any]] = None, + priority: Optional[str] = None, + created_at: Optional[float] = None, + updated_at: Optional[float] = None, + document_id: Optional[str] = None, + ) -> dict[str, Any]: + body: dict[str, Any] = { + "title": title, + "content": content, + "namespace": namespace, + } + if source_type is not None: + body["sourceType"] = source_type + if metadata is not None: + body["metadata"] = metadata + if priority is not None: + body["priority"] = priority + if created_at is not None: + body["createdAt"] = created_at + if updated_at is not None: + body["updatedAt"] = updated_at + if document_id is not None: + body["documentId"] = document_id + result = self._post("/v1/memory/documents", body) + return self._wait_for_document_ingestion(result) + + def insert_documents_batch( + self, + *, + items: Sequence[dict[str, Any]], + ) -> dict[str, Any]: + if not items: + raise ValueError("items must be a non-empty list") + result = self._post("/v1/memory/documents/batch", {"items": list(items)}) + self._wait_for_batch_ingestions(result) + return result + + def _wait_for_document_ingestion( + self, + insert_result: dict[str, Any], + *, + max_wait_seconds: int = 30, + ) -> dict[str, Any]: + """Wait for a document ingestion job to finish. + + The backend creates documents asynchronously; without polling, callers + may observe 'pending' and immediately fail list/get/query. + """ + if not isinstance(insert_result, dict): + return insert_result + + job_id = insert_result.get("jobId") or insert_result.get("job_id") + state = insert_result.get("state") or insert_result.get("status") + if not isinstance(job_id, str): + return insert_result + + pending_states = { + "pending", + "queued", + "processing", + "in_progress", + "in-progress", + "started", + "start", + } + completed_states = {"completed", "done", "succeeded", "success"} + failed_states = {"failed", "error", "cancelled", "canceled"} + + if isinstance(state, str) and state.strip().lower() in completed_states: + return insert_result + if isinstance(state, str) and state.strip().lower() not in pending_states: + # Unknown state; avoid blocking. + return insert_result + + deadline = time.time() + max_wait_seconds + last_job: dict[str, Any] | None = None + while time.time() < deadline: + last_job = self.get_ingestion_job(job_id=job_id) + job_state = ( + last_job.get("state") + or last_job.get("status") + or last_job.get("jobState") + ) + if isinstance(job_state, str): + s = job_state.strip().lower() + if s in completed_states: + return last_job + if s in failed_states: + raise AlphahumanError( + f"Ingestion job {job_id} failed (state={job_state})", + 500, + last_job, + ) + time.sleep(1.0) + + # Timeout: return original insert result so the caller can decide. + return insert_result + + def _wait_for_batch_ingestions( + self, + insert_result: dict[str, Any], + *, + max_wait_seconds: int = 30, + ) -> None: + """Wait for insert_documents_batch accepted jobs to finish.""" + if not isinstance(insert_result, dict): + return + + job_ids: list[str] = [] + + accepted = insert_result.get("accepted") + if isinstance(accepted, list): + for a in accepted: + if isinstance(a, dict): + jid = a.get("jobId") or a.get("job_id") + if isinstance(jid, str): + job_ids.append(jid) + + direct_job_id = insert_result.get("jobId") or insert_result.get("job_id") + if isinstance(direct_job_id, str): + job_ids.append(direct_job_id) + + # De-dupe while preserving order. + seen: set[str] = set() + deduped_job_ids: list[str] = [] + for jid in job_ids: + if jid not in seen: + seen.add(jid) + deduped_job_ids.append(jid) + + if not deduped_job_ids: + return + + deadline = time.time() + max_wait_seconds + remaining = set(deduped_job_ids) + completed_states = {"completed", "done", "succeeded", "success"} + failed_states = {"failed", "error", "cancelled", "canceled"} + + while remaining and time.time() < deadline: + for job_id in list(remaining): + job = self.get_ingestion_job(job_id=job_id) + job_state = ( + job.get("state") or job.get("status") or job.get("jobState") + ) + if isinstance(job_state, str): + s = job_state.strip().lower() + if s in completed_states: + remaining.remove(job_id) + elif s in failed_states: + raise AlphahumanError( + f"Ingestion job {job_id} failed (state={job_state})", + 500, + job, + ) + if remaining: + time.sleep(1.0) + + def list_documents( + self, + *, + namespace: Optional[str] = None, + limit: Optional[int] = None, + offset: Optional[int] = None, + ) -> dict[str, Any]: + params: dict[str, Any] = {} + if namespace: + params["namespace"] = namespace + if limit is not None: + params["limit"] = limit + if offset is not None: + params["offset"] = offset + return self._get("/v1/memory/documents", params if params else None) + + def get_document( + self, + *, + document_id: str, + namespace: Optional[str] = None, + ) -> dict[str, Any]: + params: dict[str, Any] = {} + if namespace: + params["namespace"] = namespace + path = f"/v1/memory/documents/{quote(document_id, safe='')}" + return self._get(path, params if params else None) + + def delete_document( + self, + *, + document_id: str, + namespace: str, + ) -> dict[str, Any]: + path = f"/v1/memory/documents/{quote(document_id, safe='')}" + return self._delete(path, {"namespace": namespace}) + + def query_memory_context( + self, + *, + query: str, + namespace: Optional[str] = None, + include_references: Optional[bool] = None, + max_chunks: Optional[int] = None, + document_ids: Optional[Sequence[str]] = None, + recall_only: Optional[bool] = None, + llm_query: Optional[str] = None, + ) -> dict[str, Any]: + body: dict[str, Any] = {"query": query} + if include_references is not None: + body["includeReferences"] = include_references + if namespace: + body["namespace"] = namespace + if max_chunks is not None: + body["maxChunks"] = max_chunks + if document_ids is not None: + body["documentIds"] = list(document_ids) + if recall_only is not None: + body["recallOnly"] = recall_only + if llm_query is not None: + body["llmQuery"] = llm_query + return self._post("/v1/memory/queries", body) + + def chat_memory_context( + self, + *, + messages: Sequence[dict[str, Any]], + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + ) -> dict[str, Any]: + body: dict[str, Any] = {"messages": list(messages)} + if temperature is not None: + body["temperature"] = temperature + if max_tokens is not None: + body["maxTokens"] = max_tokens + return self._post("/v1/memory/conversations", body) + + def record_interactions( + self, + *, + namespace: str, + entity_names: Sequence[str], + description: Optional[str] = None, + interaction_level: Optional[str] = None, + interaction_levels: Optional[Sequence[str]] = None, + timestamp: Optional[float] = None, + ) -> dict[str, Any]: + body: dict[str, Any] = { + "namespace": namespace, + "entityNames": list(entity_names), + } + if description is not None: + body["description"] = description + if interaction_level is not None: + body["interactionLevel"] = interaction_level + if interaction_levels is not None: + body["interactionLevels"] = list(interaction_levels) + if timestamp is not None: + body["timestamp"] = timestamp + return self._post("/v1/memory/interactions", body) + + def recall_thoughts( + self, + *, + namespace: Optional[str] = None, + max_chunks: Optional[int] = None, + temperature: Optional[float] = None, + randomness_seed: Optional[int] = None, + persist: Optional[bool] = None, + enable_prediction_check: Optional[bool] = None, + thought_prompt: Optional[str] = None, + ) -> dict[str, Any]: + body: dict[str, Any] = {} + if namespace: + body["namespace"] = namespace + if max_chunks is not None: + body["maxChunks"] = max_chunks + if temperature is not None: + body["temperature"] = temperature + if randomness_seed is not None: + body["randomnessSeed"] = randomness_seed + if persist is not None: + body["persist"] = persist + if enable_prediction_check is not None: + body["enablePredictionCheck"] = enable_prediction_check + if thought_prompt is not None: + body["thoughtPrompt"] = thought_prompt + return self._post("/v1/memory/memories/thoughts", body) + + def sync_memory( + self, + *, + workspace_id: str, + agent_id: str, + files: Sequence[dict[str, Any]], + source: Optional[str] = None, + ) -> dict[str, Any]: + body: dict[str, Any] = { + "workspaceId": workspace_id, + "agentId": agent_id, + "files": list(files), + } + if source is not None: + body["source"] = source + return self._post("/v1/memory/sync", body) + + def chat_memory( + self, + *, + messages: Sequence[dict[str, Any]], + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + ) -> dict[str, Any]: + body: dict[str, Any] = {"messages": list(messages)} + if temperature is not None: + body["temperature"] = temperature + if max_tokens is not None: + body["maxTokens"] = max_tokens + return self._post("/v1/memory/chat", body) + + def interact_memory( + self, + *, + namespace: str, + entity_names: Sequence[str], + description: Optional[str] = None, + interaction_level: Optional[str] = None, + interaction_levels: Optional[Sequence[str]] = None, + timestamp: Optional[float] = None, + ) -> dict[str, Any]: + body: dict[str, Any] = { + "namespace": namespace, + "entityNames": list(entity_names), + } + if description is not None: + body["description"] = description + if interaction_level is not None: + body["interactionLevel"] = interaction_level + if interaction_levels is not None: + body["interactionLevels"] = list(interaction_levels) + if timestamp is not None: + body["timestamp"] = timestamp + return self._post("/v1/memory/interact", body) + + def recall_memory_master( + self, + *, + namespace: Optional[str] = None, + max_chunks: Optional[int] = None, + ) -> dict[str, Any]: + body: dict[str, Any] = {} + if namespace: + body["namespace"] = namespace + if max_chunks is not None: + body["maxChunks"] = max_chunks + return self._post("/v1/memory/recall", body) + + def recall_memories( + self, + *, + namespace: Optional[str] = None, + top_k: Optional[float] = None, + min_retention: Optional[float] = None, + as_of: Optional[float] = None, + ) -> dict[str, Any]: + body: dict[str, Any] = {} + if namespace: + body["namespace"] = namespace + if top_k is not None: + body["topK"] = top_k + if min_retention is not None: + body["minRetention"] = min_retention + if as_of is not None: + body["asOf"] = as_of + return self._post("/v1/memory/memories/recall", body) + + def get_ingestion_job(self, *, job_id: str) -> dict[str, Any]: + path = f"/v1/memory/ingestion/jobs/{quote(job_id, safe='')}" + return self._get(path, None) + def _post(self, path: str, body: dict[str, Any]) -> dict[str, Any]: res = self._http.post(path, json=body) + return self._parse_success(res) + + def _get( + self, path: str, params: Optional[dict[str, Any]] = None + ) -> dict[str, Any]: + res = self._http.get(path, params=params) + return self._parse_success(res) + + def _delete( + self, path: str, params: Optional[dict[str, Any]] = None + ) -> dict[str, Any]: + res = self._http.delete(path, params=params) + return self._parse_success(res) + + def _parse_success(self, res: httpx.Response) -> dict[str, Any]: try: payload = res.json() except Exception: @@ -132,9 +549,50 @@ def __init__( self.save_memory, self.recall_memory, self.delete_memory, + self.sync_memory, + self.insert_document, + self.insert_documents_batch, + self.list_documents, + self.get_document, + self.delete_document, + self.query_memory_context, + self.chat_memory_context, + self.record_interactions, + self.recall_thoughts, + self.chat_memory, + self.interact_memory, + self.recall_memory_master, + self.recall_memories, + self.get_ingestion_job, ] super().__init__(name="neocortex_memory", tools=tools, **kwargs) + def _json(self, value: Any) -> str: + try: + return json.dumps(value, ensure_ascii=False, indent=2) + except Exception: + return str(value) + + def _extract_context_string(self, data: dict[str, Any], namespace: Optional[str] = None) -> str: + llm_msg = data.get("llmContextMessage") or data.get("response") + if isinstance(llm_msg, str) and llm_msg.strip(): + return llm_msg + + context = data.get("context") or {} + chunks = context.get("chunks") or [] + texts: list[str] = [] + for chunk in chunks: + if not isinstance(chunk, dict): + continue + text = chunk.get("content") or chunk.get("text") or "" + if isinstance(text, str) and text.strip(): + texts.append(text.strip()) + + if not texts: + ns = namespace or "?" + return f"No context found in namespace '{ns}'." + return "\n\n".join(texts) + def save_memory( self, key: str, @@ -239,3 +697,324 @@ def delete_memory( result = self._client.delete_memory(namespace=namespace) nodes_deleted = result.get("nodesDeleted", 0) return f"Deleted {nodes_deleted} memory node(s) from namespace '{namespace}'." + + def sync_memory( + self, + workspace_id: str, + agent_id: str, + files: Sequence[dict[str, Any]], + source: Optional[str] = None, + ) -> str: + """Sync OpenClaw memory files (POST /v1/memory/sync).""" + result = self._client.sync_memory( + workspace_id=workspace_id, + agent_id=agent_id, + files=files, + source=source, + ) + return self._json(result) + + def insert_document( + self, + title: str, + content: str, + namespace: str, + source_type: Optional[str] = None, + metadata: Optional[dict[str, Any]] = None, + priority: Optional[str] = None, + created_at: Optional[float] = None, + updated_at: Optional[float] = None, + document_id: Optional[str] = None, + ) -> str: + """Insert a single memory document (POST /v1/memory/documents).""" + result = self._client.insert_document( + title=title, + content=content, + namespace=namespace, + source_type=source_type, + metadata=metadata, + priority=priority, + created_at=created_at, + updated_at=updated_at, + document_id=document_id, + ) + return self._json(result) + + def insert_documents_batch( + self, + items: Sequence[dict[str, Any]], + ) -> str: + """Insert multiple documents (POST /v1/memory/documents/batch).""" + result = self._client.insert_documents_batch(items=items) + return self._json(result) + + def list_documents( + self, + namespace: Optional[str] = None, + limit: Optional[int] = None, + offset: Optional[int] = None, + ) -> str: + """List documents (GET /v1/memory/documents).""" + result = self._client.list_documents(namespace=namespace, limit=limit, offset=offset) + return self._json(result) + + def get_document( + self, + document_id: str, + namespace: Optional[str] = None, + ) -> str: + """Get a document (GET /v1/memory/documents/:documentId).""" + result = self._client.get_document(document_id=document_id, namespace=namespace) + return self._json(result) + + def delete_document( + self, + document_id: str, + namespace: str, + ) -> str: + """Delete a document (DELETE /v1/memory/documents/:documentId).""" + result = self._client.delete_document(document_id=document_id, namespace=namespace) + return self._json(result) + + def query_memory_context( + self, + query: str, + namespace: Optional[str] = None, + include_references: Optional[bool] = True, + max_chunks: Optional[int] = None, + document_ids: Optional[Any] = None, + recall_only: Optional[bool] = None, + llm_query: Optional[str] = None, + ) -> str: + """Query memory context (POST /v1/memory/queries).""" + # Agents can pass `{}` for optional list args; normalize to None. + normalized_document_ids: Optional[Sequence[str]] = None + if document_ids is None: + normalized_document_ids = None + elif isinstance(document_ids, dict): + if len(document_ids) == 0: + normalized_document_ids = None + else: + candidates = [ + v for v in document_ids.values() if isinstance(v, str) + ] + normalized_document_ids = candidates or None + elif isinstance(document_ids, str): + normalized_document_ids = [document_ids] + elif isinstance(document_ids, (list, tuple, set)): + candidates = [d for d in document_ids if isinstance(d, str)] + normalized_document_ids = candidates or None + else: + normalized_document_ids = None + + data = self._client.query_memory_context( + query=query, + namespace=namespace, + include_references=include_references, + max_chunks=max_chunks, + document_ids=normalized_document_ids, + recall_only=recall_only, + llm_query=llm_query, + ) + if namespace: + return self._extract_context_string(data, namespace=namespace) + return self._json(data) + + def chat_memory_context( + self, + messages: Any, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + ) -> str: + """Chat with memory context (POST /v1/memory/conversations).""" + normalized: list[dict[str, Any]] = [] + if messages is None: + normalized = [] + elif isinstance(messages, dict): + # Common bad agent output: `{}`. + if not messages: + normalized = [] + # Sometimes the agent might wrap messages in `{ "messages": [...] }`. + elif isinstance(messages.get("messages"), list): + normalized = [ + m + for m in messages["messages"] + if isinstance(m, dict) and isinstance(m.get("role"), str) and isinstance(m.get("content"), str) + ] + # Or the agent might send a single message: `{role: ..., content: ...}`. + elif isinstance(messages.get("role"), str) and isinstance(messages.get("content"), str): + normalized = [{"role": messages["role"], "content": messages["content"]}] + elif isinstance(messages, str): + normalized = [{"role": "user", "content": messages}] + elif isinstance(messages, (list, tuple)): + normalized = [ + m + for m in messages + if isinstance(m, dict) + and isinstance(m.get("role"), str) + and isinstance(m.get("content"), str) + ] + + if not normalized: + return "chat_memory_context: missing/invalid messages; expected messages=[{role: 'user', content: '...'}]." + + data = self._client.chat_memory_context( + messages=normalized, + temperature=temperature, + max_tokens=max_tokens, + ) + content = data.get("content") + if isinstance(content, str) and content.strip(): + return content + return self._json(data) + + def record_interactions( + self, + namespace: str, + entity_names: Any, + description: Optional[str] = None, + interaction_level: Optional[str] = None, + interaction_levels: Optional[Any] = None, + timestamp: Optional[float] = None, + ) -> str: + """Record interaction signals (POST /v1/memory/interactions).""" + normalized_entity_names: list[str] = [] + if isinstance(entity_names, dict): + # Common bad agent output: `{}`. + if entity_names: + normalized_entity_names = [ + v for v in entity_names.values() if isinstance(v, str) and v.strip() + ] + elif isinstance(entity_names, str): + if entity_names.strip(): + normalized_entity_names = [entity_names.strip()] + elif isinstance(entity_names, (list, tuple, set)): + normalized_entity_names = [ + v.strip() + for v in entity_names + if isinstance(v, str) and v.strip() + ] + + normalized_interaction_levels: Optional[list[str]] = None + if isinstance(interaction_levels, dict): + if interaction_levels: + candidates = [ + v for v in interaction_levels.values() if isinstance(v, str) and v.strip() + ] + normalized_interaction_levels = candidates or None + elif isinstance(interaction_levels, str): + normalized_interaction_levels = [interaction_levels.strip()] if interaction_levels.strip() else None + elif isinstance(interaction_levels, (list, tuple, set)): + candidates = [v.strip() for v in interaction_levels if isinstance(v, str) and v.strip()] + normalized_interaction_levels = candidates or None + + if not normalized_entity_names: + return "record_interactions: missing/invalid entity_names; expected entity_names=['...','...']." + + data = self._client.record_interactions( + namespace=namespace, + entity_names=normalized_entity_names, + description=description, + interaction_level=interaction_level, + interaction_levels=normalized_interaction_levels, + timestamp=timestamp, + ) + return self._json(data) + + def recall_thoughts( + self, + namespace: Optional[str] = None, + max_chunks: Optional[int] = None, + temperature: Optional[float] = None, + randomness_seed: Optional[int] = None, + persist: Optional[bool] = None, + enable_prediction_check: Optional[bool] = None, + thought_prompt: Optional[str] = None, + ) -> str: + """Generate reflective thoughts (POST /v1/memory/memories/thoughts).""" + data = self._client.recall_thoughts( + namespace=namespace, + max_chunks=max_chunks, + temperature=temperature, + randomness_seed=randomness_seed, + persist=persist, + enable_prediction_check=enable_prediction_check, + thought_prompt=thought_prompt, + ) + thought = data.get("thought") + if isinstance(thought, str) and thought.strip(): + return thought + return self._json(data) + + def chat_memory( + self, + messages: Sequence[dict[str, Any]], + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + ) -> str: + """Chat with memory (POST /v1/memory/chat).""" + data = self._client.chat_memory( + messages=messages, + temperature=temperature, + max_tokens=max_tokens, + ) + content = data.get("content") + if isinstance(content, str) and content.strip(): + return content + return self._json(data) + + def interact_memory( + self, + namespace: str, + entity_names: Sequence[str], + description: Optional[str] = None, + interaction_level: Optional[str] = None, + interaction_levels: Optional[Sequence[str]] = None, + timestamp: Optional[float] = None, + ) -> str: + """Record interactions (POST /v1/memory/interact).""" + data = self._client.interact_memory( + namespace=namespace, + entity_names=entity_names, + description=description, + interaction_level=interaction_level, + interaction_levels=interaction_levels, + timestamp=timestamp, + ) + return self._json(data) + + def recall_memory_master( + self, + namespace: Optional[str] = None, + max_chunks: Optional[int] = None, + ) -> str: + """Recall context from master node (POST /v1/memory/recall).""" + data = self._client.recall_memory_master(namespace=namespace, max_chunks=max_chunks) + if isinstance(namespace, str) and namespace: + return self._extract_context_string(data, namespace=namespace) + return self._json(data) + + def recall_memories( + self, + namespace: Optional[str] = None, + top_k: Optional[float] = None, + min_retention: Optional[float] = None, + as_of: Optional[float] = None, + ) -> str: + """Recall memories (Ebbinghaus bank) (POST /v1/memory/memories/recall).""" + data = self._client.recall_memories( + namespace=namespace, + top_k=top_k, + min_retention=min_retention, + as_of=as_of, + ) + memories = data.get("memories") + if isinstance(memories, list): + return self._json(memories) + return self._json(data) + + def get_ingestion_job(self, job_id: str) -> str: + """Get ingestion job status (GET /v1/memory/ingestion/jobs/:jobId).""" + data = self._client.get_ingestion_job(job_id=job_id) + return self._json(data) + From ca9c031322c9a10cdb6a4ca53b8d41ded9cf32a0 Mon Sep 17 00:00:00 2001 From: Shanu Date: Fri, 20 Mar 2026 19:53:45 +0530 Subject: [PATCH 3/3] nhance Raycast Neocortex plugin with new memory management features - Updated README.md to include a comprehensive list of new tools for memory synchronization, document management, and interaction recording. - Expanded the NeocortexMemoryClient class in client.ts with methods for syncing memory, inserting documents, and managing document batches. - Enhanced the RaycastNeocortexMemory class in index.ts to expose new functionalities and schemas for memory operations, ensuring type safety and clarity. - Improved the overall structure and documentation for better usability and understanding of the new features. --- packages/plugin-raycast/README.md | 32 ++- packages/plugin-raycast/src/client.ts | 291 ++++++++++++++++++++ packages/plugin-raycast/src/index.ts | 372 ++++++++++++++++++++++++++ 3 files changed, 694 insertions(+), 1 deletion(-) diff --git a/packages/plugin-raycast/README.md b/packages/plugin-raycast/README.md index c90fc57..f638b02 100644 --- a/packages/plugin-raycast/README.md +++ b/packages/plugin-raycast/README.md @@ -6,10 +6,25 @@ This package provides a small MCP server and TypeScript helpers so the [Raycast ## Features -- **MCP server for Raycast** — exposes three tools: +- **MCP server for Raycast** — exposes the full Mastra-compatible Neocortex tool surface: - `neocortex_save_memory` - `neocortex_recall_memory` - `neocortex_delete_memory` + - `neocortex_sync_memory` + - `neocortex_insert_document` + - `neocortex_insert_documents_batch` + - `neocortex_list_documents` + - `neocortex_get_document` + - `neocortex_delete_document` + - `neocortex_query_memory_context` + - `neocortex_chat_memory_context` + - `neocortex_record_interactions` + - `neocortex_recall_thoughts` + - `neocortex_chat_memory` + - `neocortex_interact_memory` + - `neocortex_recall_memory_master` + - `neocortex_recall_memories` + - `neocortex_get_ingestion_job` - **Shared client** — reuses the same `NeocortexMemoryClient` and types as the other Neocortex plugins. - **Simple bootstrap** — one helper (`runNeocortexMcpServerFromEnv`) you can point to from the Raycast MCP Extension configuration. @@ -51,6 +66,21 @@ After saving, Raycast will: - `neocortex_save_memory` - `neocortex_recall_memory` - `neocortex_delete_memory` + - `neocortex_sync_memory` + - `neocortex_insert_document` + - `neocortex_insert_documents_batch` + - `neocortex_list_documents` + - `neocortex_get_document` + - `neocortex_delete_document` + - `neocortex_query_memory_context` + - `neocortex_chat_memory_context` + - `neocortex_record_interactions` + - `neocortex_recall_thoughts` + - `neocortex_chat_memory` + - `neocortex_interact_memory` + - `neocortex_recall_memory_master` + - `neocortex_recall_memories` + - `neocortex_get_ingestion_job` - Allow you to `@-mention` the Neocortex server and invoke its tools in AI Chat, Commands, and Presets. ## Programmatic Usage diff --git a/packages/plugin-raycast/src/client.ts b/packages/plugin-raycast/src/client.ts index 932c0b3..4908606 100644 --- a/packages/plugin-raycast/src/client.ts +++ b/packages/plugin-raycast/src/client.ts @@ -57,6 +57,297 @@ export class NeocortexMemoryClient { return this.post("/v1/memory/admin/delete", body); } + async syncMemory(params: { + workspace_id: string; + agent_id: string; + source?: "startup" | "agent_end"; + files: Array<{ + file_path?: string; + filePath?: string; + content: string; + timestamp?: string | number; + hash: string; + }>; + }): Promise { + const body = { + workspaceId: params.workspace_id, + agentId: params.agent_id, + source: params.source, + files: (params.files ?? []).map((f) => ({ + filePath: f.file_path ?? f.filePath, + content: f.content, + timestamp: String(f.timestamp ?? ""), + hash: f.hash, + })), + }; + return this.post("/v1/memory/sync", body); + } + + async insertDocument(params: { + title: string; + content: string; + namespace: string; + source_type?: string; + metadata?: Record; + priority?: string; + created_at?: number; + updated_at?: number; + document_id?: string; + }): Promise { + const body = { + title: params.title, + content: params.content, + namespace: params.namespace, + sourceType: params.source_type ?? "doc", + metadata: params.metadata ?? {}, + priority: params.priority, + createdAt: params.created_at, + updatedAt: params.updated_at, + documentId: params.document_id, + }; + return this.post("/v1/memory/documents", body); + } + + async insertDocumentsBatch(params: { + items: Array<{ + title: string; + content: string; + namespace: string; + source_type?: string; + metadata?: Record; + priority?: string; + created_at?: number; + updated_at?: number; + document_id?: string; + }>; + }): Promise { + const items = (params.items ?? []).map((it) => ({ + title: it.title, + content: it.content, + namespace: it.namespace, + sourceType: it.source_type ?? "doc", + metadata: it.metadata ?? {}, + priority: it.priority, + createdAt: it.created_at, + updatedAt: it.updated_at, + documentId: it.document_id, + })); + return this.post("/v1/memory/documents/batch", { items }); + } + + async listDocuments(params: { + namespace?: string; + limit?: number; + offset?: number; + }): Promise { + return this.get("/v1/memory/documents", params); + } + + async getDocument(params: { document_id: string; namespace?: string }): Promise { + return this.get(`/v1/memory/documents/${encodeURIComponent(params.document_id)}`, { + namespace: params.namespace, + }); + } + + async deleteDocument(params: { document_id: string; namespace: string }): Promise { + return this.delete(`/v1/memory/documents/${encodeURIComponent(params.document_id)}`, { + namespace: params.namespace, + }); + } + + async queryMemoryContext(params: { + query: string; + namespace?: string; + include_references?: boolean; + max_chunks?: number; + document_ids?: string[]; + recall_only?: boolean; + llm_query?: string; + }): Promise { + return this.post("/v1/memory/queries", { + query: params.query, + includeReferences: params.include_references, + namespace: params.namespace, + maxChunks: params.max_chunks, + documentIds: params.document_ids, + recallOnly: params.recall_only, + llmQuery: params.llm_query, + }); + } + + async chatMemoryContext(params: { + messages: Array<{ role: string; content: string }>; + temperature?: number; + max_tokens?: number; + }): Promise { + return this.post("/v1/memory/conversations", { + messages: params.messages, + temperature: params.temperature, + maxTokens: params.max_tokens, + }); + } + + async recordInteractions(params: { + namespace: string; + entity_names: string[]; + description?: string; + interaction_level?: string; + interaction_levels?: string[]; + timestamp?: number; + }): Promise { + return this.post("/v1/memory/interactions", { + namespace: params.namespace, + entityNames: params.entity_names, + description: params.description, + interactionLevel: params.interaction_level, + interactionLevels: params.interaction_levels, + timestamp: params.timestamp, + }); + } + + async recallThoughts(params: { + namespace?: string; + max_chunks?: number; + temperature?: number; + randomness_seed?: number; + persist?: boolean; + enable_prediction_check?: boolean; + thought_prompt?: string; + }): Promise { + return this.post("/v1/memory/memories/thoughts", { + namespace: params.namespace, + maxChunks: params.max_chunks, + temperature: params.temperature, + randomnessSeed: params.randomness_seed, + persist: params.persist, + enablePredictionCheck: params.enable_prediction_check, + thoughtPrompt: params.thought_prompt, + }); + } + + async chatMemory(params: { + messages: Array<{ role: string; content: string }>; + temperature?: number; + max_tokens?: number; + }): Promise { + return this.post("/v1/memory/chat", { + messages: params.messages, + temperature: params.temperature, + maxTokens: params.max_tokens, + }); + } + + async interactMemory(params: { + namespace: string; + entity_names: string[]; + description?: string; + interaction_level?: string; + interaction_levels?: string[]; + timestamp?: number; + }): Promise { + return this.post("/v1/memory/interact", { + namespace: params.namespace, + entityNames: params.entity_names, + description: params.description, + interactionLevel: params.interaction_level, + interactionLevels: params.interaction_levels, + timestamp: params.timestamp, + }); + } + + async recallMemoryMaster(params: { namespace?: string; max_chunks?: number }): Promise { + return this.post("/v1/memory/recall", { + namespace: params.namespace, + maxChunks: params.max_chunks, + }); + } + + async recallMemories(params: { + namespace?: string; + top_k?: number; + min_retention?: number; + as_of?: number; + }): Promise { + return this.post("/v1/memory/memories/recall", { + namespace: params.namespace, + topK: params.top_k, + minRetention: params.min_retention, + asOf: params.as_of, + }); + } + + async getIngestionJob(params: { job_id: string }): Promise { + return this.get( + `/v1/memory/ingestion/jobs/${encodeURIComponent(params.job_id)}`, + undefined, + ); + } + + private buildQuery(params: Record | undefined): string { + if (!params) return ""; + const qs = new URLSearchParams( + Object.entries(params) + .filter(([, v]) => v !== undefined) + .map(([k, v]) => [k, Array.isArray(v) ? JSON.stringify(v) : String(v)]), + ).toString(); + return qs ? `?${qs}` : ""; + } + + private async get(path: string, params?: Record): Promise { + const url = `${this.baseUrl}${path}${this.buildQuery(params)}`; + const res = await fetch(url, { + method: "GET", + headers: { + Authorization: `Bearer ${this.token}`, + }, + }); + + const text = await res.text(); + let json: any; + try { + json = text ? JSON.parse(text) : {}; + } catch { + throw new Error(`HTTP ${res.status}: Non-JSON response`); + } + + if (!res.ok || (json as any).success === false) { + const msg = (json as any).error || `HTTP ${res.status}`; + this.logger?.error?.("Neocortex API error", { status: res.status, body: json }); + throw new Error(msg); + } + + return json as T; + } + + private async delete( + path: string, + params?: Record, + ): Promise { + const url = `${this.baseUrl}${path}${this.buildQuery(params)}`; + const res = await fetch(url, { + method: "DELETE", + headers: { + Authorization: `Bearer ${this.token}`, + }, + }); + + const text = await res.text(); + let json: any; + try { + json = text ? JSON.parse(text) : {}; + } catch { + throw new Error(`HTTP ${res.status}: Non-JSON response`); + } + + if (!res.ok || (json as any).success === false) { + const msg = (json as any).error || `HTTP ${res.status}`; + this.logger?.error?.("Neocortex API error", { status: res.status, body: json }); + throw new Error(msg); + } + + return json as T; + } + private async post(path: string, body: Record): Promise { const url = `${this.baseUrl}${path}`; const res = await fetch(url, { diff --git a/packages/plugin-raycast/src/index.ts b/packages/plugin-raycast/src/index.ts index d160b28..d748a69 100644 --- a/packages/plugin-raycast/src/index.ts +++ b/packages/plugin-raycast/src/index.ts @@ -89,6 +89,120 @@ export class RaycastNeocortexMemory { }; } + private extractContext(data: any): string { + const llmMsg = data?.llmContextMessage || data?.response; + if (typeof llmMsg === "string" && llmMsg.trim()) return llmMsg.trim(); + + const chunks = data?.context?.chunks ?? []; + if (!Array.isArray(chunks) || chunks.length === 0) { + return "No relevant memories found."; + } + + const texts: string[] = []; + for (const chunk of chunks) { + if (!chunk || typeof chunk !== "object") continue; + const text = + (chunk as any).content ?? (chunk as any).text ?? (chunk as any).body ?? ""; + if (typeof text === "string" && text.trim()) texts.push(text.trim()); + } + + return texts.length ? texts.join("\n\n") : "No relevant memories found."; + } + + async syncMemory(input: any) { + const raw = await this.client.syncMemory(input); + return { ok: true as const, raw }; + } + + async insertDocument(input: any) { + const raw = await this.client.insertDocument(input); + return { ok: true as const, raw }; + } + + async insertDocumentsBatch(input: any) { + const raw = await this.client.insertDocumentsBatch(input); + return { ok: true as const, raw }; + } + + async listDocuments(input: any) { + const raw = await this.client.listDocuments(input); + return { ok: true as const, raw }; + } + + async getDocument(input: any) { + const raw = await this.client.getDocument(input); + return { ok: true as const, raw }; + } + + async deleteDocument(input: any) { + const raw = await this.client.deleteDocument(input); + return { ok: true as const, raw }; + } + + async queryMemoryContext(input: any) { + const namespace = this.resolveNamespace(input.namespace); + const rawRes = await this.client.queryMemoryContext({ ...input, namespace }); + const data = rawRes?.data ?? rawRes; + return { + ok: true as const, + namespace, + context: this.extractContext(data), + raw: data, + }; + } + + async chatMemoryContext(input: any) { + const rawRes = await this.client.chatMemoryContext(input); + const data = rawRes?.data ?? rawRes; + const content = typeof data?.content === "string" ? data.content : ""; + return { ok: true as const, content, raw: data }; + } + + async recordInteractions(input: any) { + const raw = await this.client.recordInteractions(input); + return { ok: true as const, raw }; + } + + async recallThoughts(input: any) { + const rawRes = await this.client.recallThoughts(input); + const data = rawRes?.data ?? rawRes; + return { ok: true as const, thought: data?.thought, raw: data }; + } + + async chatMemory(input: any) { + const rawRes = await this.client.chatMemory(input); + const data = rawRes?.data ?? rawRes; + const content = typeof data?.content === "string" ? data.content : ""; + return { ok: true as const, content, raw: data }; + } + + async interactMemory(input: any) { + const raw = await this.client.interactMemory(input); + return { ok: true as const, raw }; + } + + async recallMemoryMaster(input: any) { + const namespace = this.resolveNamespace(input.namespace); + const rawRes = await this.client.recallMemoryMaster({ ...input, namespace }); + const data = rawRes?.data ?? rawRes; + return { + ok: true as const, + namespace, + context: this.extractContext(data), + raw: data, + }; + } + + async recallMemories(input: any) { + const raw = await this.client.recallMemories(input); + return { ok: true as const, raw }; + } + + async getIngestionJob(input: any) { + const raw = await this.client.getIngestionJob(input); + return { ok: true as const, raw }; + } + /** * MCP tool definitions that can be registered on a Model Context Protocol server * for consumption by Raycast via the Raycast MCP extension. @@ -118,6 +232,114 @@ export class RaycastNeocortexMemory { namespace: z.string().optional().describe("Namespace to delete."), }); + const syncMemorySchema = z.object({ + workspace_id: z.string().describe("Workspace identifier."), + agent_id: z.string().describe("Agent identifier."), + source: z.enum(["startup", "agent_end"]).optional(), + files: z.array( + z.object({ + file_path: z.string().optional(), + content: z.string(), + timestamp: z.union([z.string(), z.number()]).optional(), + hash: z.string(), + }) + ), + }); + + const insertDocumentSchema = z.object({ + title: z.string(), + content: z.string(), + namespace: z.string(), + source_type: z.string().optional(), + metadata: z.record(z.unknown()).optional(), + priority: z.string().optional(), + created_at: z.number().optional(), + updated_at: z.number().optional(), + document_id: z.string().optional(), + }); + + const insertDocumentsBatchSchema = z.object({ + items: z.array(insertDocumentSchema), + }); + + const listDocumentsSchema = z.object({ + namespace: z.string().optional(), + limit: z.number().int().optional(), + offset: z.number().int().optional(), + }); + + const getDocumentSchema = z.object({ + document_id: z.string(), + namespace: z.string().optional(), + }); + + const deleteDocumentSchema = z.object({ + document_id: z.string(), + namespace: z.string(), + }); + + const queryMemoryContextSchema = z.object({ + query: z.string(), + namespace: z.string().optional(), + include_references: z.boolean().optional(), + max_chunks: z.number().int().optional(), + document_ids: z.array(z.string()).optional(), + recall_only: z.boolean().optional(), + llm_query: z.string().optional(), + }); + + const chatMemoryContextSchema = z.object({ + messages: z.array(z.object({ role: z.string(), content: z.string() })), + temperature: z.number().optional(), + max_tokens: z.number().int().optional(), + }); + + const recordInteractionsSchema = z.object({ + namespace: z.string(), + entity_names: z.array(z.string()), + description: z.string().optional(), + interaction_level: z.string().optional(), + interaction_levels: z.array(z.string()).optional(), + timestamp: z.number().optional(), + }); + + const recallThoughtsSchema = z.object({ + namespace: z.string().optional(), + max_chunks: z.number().int().optional(), + temperature: z.number().optional(), + randomness_seed: z.number().int().optional(), + persist: z.boolean().optional(), + enable_prediction_check: z.boolean().optional(), + thought_prompt: z.string().optional(), + }); + + const chatMemorySchema = chatMemoryContextSchema; + + const interactMemorySchema = z.object({ + namespace: z.string(), + entity_names: z.array(z.string()), + description: z.string().optional(), + interaction_level: z.string().optional(), + interaction_levels: z.array(z.string()).optional(), + timestamp: z.number().optional(), + }); + + const recallMemoryMasterSchema = z.object({ + namespace: z.string().optional(), + max_chunks: z.number().int().optional(), + }); + + const recallMemoriesSchema = z.object({ + namespace: z.string().optional(), + top_k: z.number().optional(), + min_retention: z.number().optional(), + as_of: z.number().optional(), + }); + + const getIngestionJobSchema = z.object({ + job_id: z.string(), + }); + return [ { name: "neocortex_save_memory", @@ -149,6 +371,156 @@ export class RaycastNeocortexMemory { return await memory.deleteMemory(input); }, }, + { + name: "neocortex_sync_memory", + description: "Sync OpenClaw memory files (POST /v1/memory/sync).", + inputSchema: syncMemorySchema, + async handler(args: unknown, { server }: { server: McpServer }) { + const input = syncMemorySchema.parse(args); + const memory = (server as any).__memory as RaycastNeocortexMemory; + return await memory.syncMemory(input); + }, + }, + { + name: "neocortex_insert_document", + description: "Insert a single memory document (POST /v1/memory/documents).", + inputSchema: insertDocumentSchema, + async handler(args: unknown, { server }: { server: McpServer }) { + const input = insertDocumentSchema.parse(args); + const memory = (server as any).__memory as RaycastNeocortexMemory; + return await memory.insertDocument(input); + }, + }, + { + name: "neocortex_insert_documents_batch", + description: "Insert multiple memory documents (POST /v1/memory/documents/batch).", + inputSchema: insertDocumentsBatchSchema, + async handler(args: unknown, { server }: { server: McpServer }) { + const input = insertDocumentsBatchSchema.parse(args); + const memory = (server as any).__memory as RaycastNeocortexMemory; + return await memory.insertDocumentsBatch(input); + }, + }, + { + name: "neocortex_list_documents", + description: "List ingested documents (GET /v1/memory/documents).", + inputSchema: listDocumentsSchema, + async handler(args: unknown, { server }: { server: McpServer }) { + const input = listDocumentsSchema.parse(args); + const memory = (server as any).__memory as RaycastNeocortexMemory; + return await memory.listDocuments(input); + }, + }, + { + name: "neocortex_get_document", + description: "Get a memory document (GET /v1/memory/documents/:documentId).", + inputSchema: getDocumentSchema, + async handler(args: unknown, { server }: { server: McpServer }) { + const input = getDocumentSchema.parse(args); + const memory = (server as any).__memory as RaycastNeocortexMemory; + return await memory.getDocument(input); + }, + }, + { + name: "neocortex_delete_document", + description: "Delete a memory document (DELETE /v1/memory/documents/:documentId?namespace=...).", + inputSchema: deleteDocumentSchema, + async handler(args: unknown, { server }: { server: McpServer }) { + const input = deleteDocumentSchema.parse(args); + const memory = (server as any).__memory as RaycastNeocortexMemory; + return await memory.deleteDocument(input); + }, + }, + { + name: "neocortex_query_memory_context", + description: "Query memory context (POST /v1/memory/queries).", + inputSchema: queryMemoryContextSchema, + async handler(args: unknown, { server }: { server: McpServer }) { + const input = queryMemoryContextSchema.parse(args); + const memory = (server as any).__memory as RaycastNeocortexMemory; + return await memory.queryMemoryContext(input); + }, + }, + { + name: "neocortex_chat_memory_context", + description: "Chat with memory context (POST /v1/memory/conversations).", + inputSchema: chatMemoryContextSchema, + async handler(args: unknown, { server }: { server: McpServer }) { + const input = chatMemoryContextSchema.parse(args); + const memory = (server as any).__memory as RaycastNeocortexMemory; + return await memory.chatMemoryContext(input); + }, + }, + { + name: "neocortex_record_interactions", + description: "Record interactions (POST /v1/memory/interactions).", + inputSchema: recordInteractionsSchema, + async handler(args: unknown, { server }: { server: McpServer }) { + const input = recordInteractionsSchema.parse(args); + const memory = (server as any).__memory as RaycastNeocortexMemory; + return await memory.recordInteractions(input); + }, + }, + { + name: "neocortex_recall_thoughts", + description: "Generate reflective thoughts (POST /v1/memory/memories/thoughts).", + inputSchema: recallThoughtsSchema, + async handler(args: unknown, { server }: { server: McpServer }) { + const input = recallThoughtsSchema.parse(args); + const memory = (server as any).__memory as RaycastNeocortexMemory; + return await memory.recallThoughts(input); + }, + }, + { + name: "neocortex_chat_memory", + description: "Chat with memory cache (POST /v1/memory/chat).", + inputSchema: chatMemorySchema, + async handler(args: unknown, { server }: { server: McpServer }) { + const input = chatMemorySchema.parse(args); + const memory = (server as any).__memory as RaycastNeocortexMemory; + return await memory.chatMemory(input); + }, + }, + { + name: "neocortex_interact_memory", + description: "Record entity interactions (POST /v1/memory/interact).", + inputSchema: interactMemorySchema, + async handler(args: unknown, { server }: { server: McpServer }) { + const input = interactMemorySchema.parse(args); + const memory = (server as any).__memory as RaycastNeocortexMemory; + return await memory.interactMemory(input); + }, + }, + { + name: "neocortex_recall_memory_master", + description: "Recall context from the master node (POST /v1/memory/recall).", + inputSchema: recallMemoryMasterSchema, + async handler(args: unknown, { server }: { server: McpServer }) { + const input = recallMemoryMasterSchema.parse(args); + const memory = (server as any).__memory as RaycastNeocortexMemory; + return await memory.recallMemoryMaster(input); + }, + }, + { + name: "neocortex_recall_memories", + description: "Recall memories from the Ebbinghaus bank (POST /v1/memory/memories/recall).", + inputSchema: recallMemoriesSchema, + async handler(args: unknown, { server }: { server: McpServer }) { + const input = recallMemoriesSchema.parse(args); + const memory = (server as any).__memory as RaycastNeocortexMemory; + return await memory.recallMemories(input); + }, + }, + { + name: "neocortex_get_ingestion_job", + description: "Get ingestion job status (GET /v1/memory/ingestion/jobs/:jobId).", + inputSchema: getIngestionJobSchema, + async handler(args: unknown, { server }: { server: McpServer }) { + const input = getIngestionJobSchema.parse(args); + const memory = (server as any).__memory as RaycastNeocortexMemory; + return await memory.getIngestionJob(input); + }, + }, ]; } }