permamind
Version:
An MCP server that provides an immortal memory layer for AI agents and clients
510 lines (509 loc) • 21.9 kB
JavaScript
import { aoMessageService, } from "./AOMessageService.js";
import { defaultProcessService, } from "./DefaultProcessService.js";
import { TokenProcessTemplateService } from "./TokenProcessTemplateService.js";
const WRITE_KEYWORDS = [
"send",
"transfer",
"create",
"update",
"delete",
"set",
"add",
"remove",
"mint",
"burn",
"stake",
"withdraw",
"deposit",
"register",
"vote",
];
/* const READ_KEYWORDS = [
"get",
"fetch",
"read",
"check",
"balance",
"info",
"status",
"list",
"query",
"view",
"show",
"find",
]; */
const service = () => {
return {
buildAOMessage: (processId, handler, parameters) => {
const tags = [
{
name: "Action",
value: handler.action,
},
];
for (const [key, value] of Object.entries(parameters)) {
if (value !== undefined && value !== null) {
tags.push({
name: key.charAt(0).toUpperCase() + key.slice(1),
value: String(value),
});
}
}
return {
data: typeof parameters.data === "string" ? parameters.data : undefined,
isWrite: handler.isWrite,
processId,
tags,
};
},
detectProcessType: async (processId, sampleRequests) => {
try {
// For now, we'll implement basic detection logic
// Future enhancement: could query the process to get handler list
if (sampleRequests) {
// Analyze sample requests to determine process type
const tokenRequestCount = sampleRequests.filter((req) => defaultProcessService.canHandleRequest(req)).length;
if (tokenRequestCount > 0) {
const tokenTemplate = defaultProcessService.getDefaultProcess("token", processId);
if (tokenTemplate) {
return {
confidence: Math.min(tokenRequestCount / sampleRequests.length + 0.3, 1.0),
suggestedHandlers: tokenTemplate.handlers.map((h) => h.action),
template: tokenTemplate,
type: "token",
};
}
}
}
// Future: Could send a test message to the process to detect capabilities
return null;
}
catch {
// Process type detection failed silently for MCP compatibility
return null;
}
},
executeProcessRequest: async (processMarkdown, processId, userRequest, signer) => {
try {
const processDefinition = service().parseMarkdown(processMarkdown);
processDefinition.processId = processId;
const handlerMatch = service().matchRequestToHandler(userRequest, processDefinition.handlers);
if (!handlerMatch) {
return {
error: "Could not match request to any available handler",
success: false,
};
}
const aoMessage = service().buildAOMessage(processId, handlerMatch.handler, handlerMatch.parameters);
const response = await aoMessageService.executeMessage(signer, aoMessage);
return service().interpretResponse(response, handlerMatch.handler);
}
catch (error) {
return {
error: error instanceof Error ? error.message : "Unknown error",
success: false,
};
}
},
executeSmartRequest: async (processId, userRequest, signer, processMarkdown, embeddedTemplates) => {
try {
// If markdown is provided, use traditional approach
if (processMarkdown) {
return await service().executeProcessRequest(processMarkdown, processId, userRequest, signer);
}
// Try embedded templates first if available
if (embeddedTemplates && embeddedTemplates.has("token")) {
const tokenTemplate = embeddedTemplates.get("token");
if (tokenTemplate) {
// First try enhanced NLS patterns for token operations
const tokenNLSResult = TokenProcessTemplateService.processTokenRequest(userRequest, processId);
if (tokenNLSResult && tokenNLSResult.confidence > 0.7) {
// Find the handler in the template
const handler = tokenNLSResult.template.handlers.find((h) => h.action.toLowerCase() ===
tokenNLSResult.operation.toLowerCase());
if (handler) {
// Map NLS parameters to handler parameter names
const mappedParameters = mapNLSParametersToHandler(tokenNLSResult.parameters, tokenNLSResult.operation);
const aoMessage = service().buildAOMessage(processId, handler, mappedParameters);
const response = await aoMessageService.executeMessage(signer, aoMessage);
const result = service().interpretResponse(response, handler);
return {
...result,
confidence: tokenNLSResult.confidence,
processType: "token",
suggestions: defaultProcessService.getSuggestedOperations("token"),
templateUsed: "embedded-nls",
};
}
}
// Fallback to standard handler matching
const processTemplate = {
...tokenTemplate,
processId,
};
const handlerMatch = service().matchRequestToHandler(userRequest, processTemplate.handlers);
if (handlerMatch && handlerMatch.confidence > 0.6) {
const aoMessage = service().buildAOMessage(processId, handlerMatch.handler, handlerMatch.parameters);
const response = await aoMessageService.executeMessage(signer, aoMessage);
const result = service().interpretResponse(response, handlerMatch.handler);
return {
...result,
confidence: handlerMatch.confidence,
processType: "token",
suggestions: defaultProcessService.getSuggestedOperations("token"),
templateUsed: "embedded",
};
}
}
}
// Try enhanced natural language service with auto-detection
const nlsResult = defaultProcessService.processNaturalLanguage(userRequest, processId);
if (nlsResult && nlsResult.confidence > 0.6) {
// Find the handler in the template
const handler = nlsResult.template.handlers.find((h) => h.action === nlsResult.operation);
if (handler) {
const aoMessage = service().buildAOMessage(processId, handler, nlsResult.parameters);
const response = await aoMessageService.executeMessage(signer, aoMessage);
const result = service().interpretResponse(response, handler);
return {
...result,
confidence: nlsResult.confidence,
processType: nlsResult.processType,
suggestions: defaultProcessService.getSuggestedOperations(nlsResult.processType),
templateUsed: "default",
};
}
}
// Fallback: try to detect process type and suggest operations
const canHandle = defaultProcessService.canHandleRequest(userRequest);
if (canHandle) {
return {
error: "Request appears to be a token operation, but process type could not be confirmed. Please provide process documentation or use executeTokenRequest for token operations.",
success: false,
suggestions: defaultProcessService.getSuggestedOperations("token"),
};
}
return {
error: "Could not process request. Please provide process documentation using processMarkdown parameter.",
success: false,
};
}
catch (error) {
return {
error: error instanceof Error ? error.message : "Unknown error",
success: false,
};
}
},
interpretResponse: (response, handler) => {
if (!response.success) {
return {
error: response.error || "Process execution failed",
handlerUsed: handler.action,
success: false,
};
}
let interpretedData = response.data;
// Handle AO message structure with Data field
if (response.data &&
typeof response.data === "object" &&
"Data" in response.data &&
typeof response.data.Data === "string") {
try {
const jsonData = JSON.parse(response.data.Data);
interpretedData = jsonData;
// Token-specific response handling
if (handler.action === "balance" && jsonData.Balance !== undefined) {
// For balance queries, return structured balance information
interpretedData = {
account: jsonData.Account || "unknown",
balance: jsonData.Balance,
rawData: jsonData,
ticker: jsonData.Ticker || "unknown",
};
}
else if (handler.action === "info" && jsonData.Name !== undefined) {
// For info queries, return structured token information
interpretedData = {
burnable: jsonData.Burnable,
denomination: jsonData.Denomination,
description: jsonData.Description,
logo: jsonData.Logo,
mintingStrategy: jsonData.MintingStrategy,
name: jsonData.Name,
owner: jsonData.Owner,
processId: jsonData.ProcessId,
rawData: jsonData,
ticker: jsonData.Ticker,
totalSupply: jsonData.TotalSupply,
transferable: jsonData.Transferable,
};
}
}
catch {
// Fall back to raw data if JSON parsing fails
interpretedData = response.data.Data;
}
}
return {
data: interpretedData,
handlerUsed: handler.action,
success: true,
};
},
matchRequestToHandler: (request, handlers) => {
const requestLower = request.toLowerCase();
let bestMatch = null;
let highestScore = 0;
for (const handler of handlers) {
const score = calculateMatchScore(requestLower, handler);
if (score > highestScore && score > 0.3) {
const parameters = extractParameters(request, handler);
bestMatch = {
confidence: score,
handler,
parameters,
};
highestScore = score;
}
}
return bestMatch;
},
parseMarkdown: (markdown) => {
const lines = markdown.split("");
const handlers = [];
let currentHandler = null;
let processName = "Unknown Process";
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith("# ")) {
processName = line.substring(2).trim();
}
else if (line.startsWith("## ")) {
if (currentHandler && currentHandler.action) {
handlers.push(currentHandler);
}
const action = line.substring(3).trim();
currentHandler = {
action,
description: "",
examples: [],
isWrite: isWriteAction(action),
parameters: [],
};
}
else if (currentHandler && line.startsWith("- ")) {
const paramLine = line.substring(2).trim();
const parameter = parseParameter(paramLine);
if (parameter) {
currentHandler.parameters = currentHandler.parameters || [];
currentHandler.parameters.push(parameter);
}
}
else if (currentHandler && line && !line.startsWith("#")) {
currentHandler.description = currentHandler.description
? currentHandler.description + " " + line
: line;
}
}
if (currentHandler && currentHandler.action) {
handlers.push(currentHandler);
}
return {
handlers,
name: processName,
processId: "",
};
},
};
};
const isWriteAction = (action) => {
const actionLower = action.toLowerCase();
return WRITE_KEYWORDS.some((keyword) => actionLower.includes(keyword));
};
const parseParameter = (paramLine) => {
const colonIndex = paramLine.indexOf(":");
if (colonIndex === -1)
return null;
const name = paramLine.substring(0, colonIndex).trim();
const description = paramLine.substring(colonIndex + 1).trim();
const required = !description.toLowerCase().includes("optional");
let type = "string";
if (description.toLowerCase().includes("number")) {
type = "number";
}
else if (description.toLowerCase().includes("boolean")) {
type = "boolean";
}
else if (description.toLowerCase().includes("object")) {
type = "object";
}
return {
description,
name,
required,
type,
};
};
const calculateMatchScore = (request, handler) => {
let score = 0;
// Check if action name is in request
if (request.includes(handler.action.toLowerCase())) {
score += 0.5;
}
// Check for action synonyms
const actionSynonyms = {
balance: ["check", "get", "show"],
transfer: ["send", "give", "pay"],
};
const synonyms = actionSynonyms[handler.action.toLowerCase()] || [];
for (const synonym of synonyms) {
if (request.includes(synonym)) {
score += 0.4; // Slightly less than exact match
break;
}
}
const descriptionWords = handler.description.toLowerCase().split(" ");
const requestWords = request.split(" ");
for (const word of requestWords) {
if (descriptionWords.includes(word)) {
score += 0.1;
}
}
for (const param of handler.parameters) {
if (request.includes(param.name.toLowerCase())) {
score += 0.2;
}
}
return Math.min(score, 1.0);
};
const extractParameters = (request, handler) => {
const parameters = {};
const requestLower = request.toLowerCase();
for (const param of handler.parameters) {
const value = extractParameterValue(requestLower, param.name.toLowerCase(), param.type);
if (value !== null) {
parameters[param.name] = value;
}
}
return parameters;
};
const extractParameterValue = (request, paramName, paramType) => {
// Parameter-specific patterns first
const specificPatterns = [
new RegExp(`${paramName}\s*[=:]\s*["']?([^"'\s]+)["']?`, "i"),
new RegExp(`${paramName}\s+([^\s]+)`, "i"),
];
// Check parameter-specific patterns first
for (const pattern of specificPatterns) {
const match = request.match(pattern);
if (match && match[1]) {
const value = match[1];
if (paramType === "number") {
const num = parseFloat(value);
return isNaN(num) ? null : num;
}
return value;
}
}
// Type-specific fallback patterns based on parameter type and common patterns
if (paramType === "number") {
// Look for numbers in various contexts
const numberPatterns = [
new RegExp(`send\s+([0-9.]+)`, "i"),
new RegExp(`transfer\s+([0-9.]+)`, "i"),
new RegExp(`amount\s*[=:]?\s*([0-9.]+)`, "i"),
new RegExp(`([0-9.]+)\s+tokens?`, "i"),
new RegExp(`([0-9.]+)\s+to`, "i"), // amount before "to"
new RegExp(`([0-9.]+)`), // Last resort: any number
];
for (const pattern of numberPatterns) {
const match = request.match(pattern);
if (match && match[1]) {
const num = parseFloat(match[1]);
if (!isNaN(num)) {
return num;
}
}
}
}
else if (paramType === "string") {
// Handle different string parameter types
if (paramName === "recipient" || paramName === "to") {
// Address/recipient patterns
const addressPatterns = [
/to\s+([^\s]+)/i,
/recipient\s+([^\s]+)/i,
/send\s+[0-9.]+\s+(?:tokens?\s+)?to\s+([^\s]+)/i, // "send X tokens to alice"
/transfer\s+[0-9.]+\s+(?:tokens?\s+)?to\s+([^\s]+)/i, // "transfer X tokens to alice"
];
for (const pattern of addressPatterns) {
const match = request.match(pattern);
if (match && match[1]) {
return match[1];
}
}
}
else if (paramName === "account" || paramName === "address") {
// Account/address patterns for balance checks etc.
const accountPatterns = [
new RegExp(`account\s+([^\s]+)`, "i"),
new RegExp(`address\s+([^\s]+)`, "i"),
new RegExp(`for\s+([^\s]+)`, "i"), // "balance for alice"
new RegExp(`of\s+([^\s]+)`, "i"), // "balance of alice"
];
for (const pattern of accountPatterns) {
const match = request.match(pattern);
if (match && match[1]) {
return match[1];
}
}
}
}
return null;
};
/**
* Map NLS parameters to handler parameter names
* Handles the parameter name mapping between NLS patterns and handler definitions
*/
const mapNLSParametersToHandler = (nlsParameters, operation) => {
const mappedParameters = {};
// Handle different operations and their parameter mappings
switch (operation.toLowerCase()) {
case "balance":
// NLS: { account } -> Handler: { Target }
if (nlsParameters.account) {
mappedParameters.Target = nlsParameters.account;
}
break;
case "burn":
// NLS: { amount } -> Handler: { Quantity }
if (nlsParameters.amount) {
mappedParameters.Quantity = String(nlsParameters.amount);
}
break;
case "mint":
// NLS: { recipient, amount } -> Handler: { Recipient, Quantity }
if (nlsParameters.recipient) {
mappedParameters.Recipient = nlsParameters.recipient;
}
if (nlsParameters.amount) {
mappedParameters.Quantity = String(nlsParameters.amount);
}
break;
case "transfer":
// NLS: { recipient, amount } -> Handler: { Recipient, Quantity }
if (nlsParameters.recipient) {
mappedParameters.Recipient = nlsParameters.recipient;
}
if (nlsParameters.amount) {
mappedParameters.Quantity = String(nlsParameters.amount);
}
break;
default:
// For other operations, pass through as-is
return nlsParameters;
}
return mappedParameters;
};
export const processCommunicationService = service();