@memberjunction/a2aserver
Version:
MemberJunction Agent-To-Agent (A2A) Server Implementation
458 lines • 16.6 kB
JavaScript
import { Metadata } from "@memberjunction/core";
import { setupSQLServerClient, SQLServerProviderConfigData, UserCache } from "@memberjunction/sqlserver-dataprovider";
import express from 'express';
import * as sql from 'mssql';
import { configInfo, dbDatabase, dbHost, dbPassword, dbPort, dbUsername, dbInstanceName, dbTrustServerCertificate, a2aServerSettings } from './config.js';
import { EntityOperations } from './EntityOperations.js';
// A2A Server Configuration
const a2aServerPort = a2aServerSettings?.port || 3200;
// Database Configuration
const poolConfig = {
server: dbHost,
port: dbPort,
user: dbUsername,
password: dbPassword,
database: dbDatabase,
requestTimeout: configInfo.databaseSettings.requestTimeout,
connectionTimeout: configInfo.databaseSettings.connectionTimeout,
options: {
encrypt: true,
enableArithAbort: true,
trustServerCertificate: dbTrustServerCertificate === 'Y'
},
};
if (dbInstanceName !== null && dbInstanceName !== undefined && dbInstanceName.trim().length > 0) {
poolConfig.options.instanceName = dbInstanceName;
}
// In-memory storage for tasks (in production, this would use a database)
const tasks = new Map();
// Express application
const app = express();
app.use(express.json());
// Initialize A2A server
export async function initializeA2AServer() {
try {
if (!a2aServerSettings?.enableA2AServer) {
console.log("A2A Server is disabled in the configuration.");
throw new Error("A2A Server is disabled in the configuration.");
}
// Initialize database connection
const pool = new sql.ConnectionPool(poolConfig);
await pool.connect();
// Setup SQL Server client
const config = new SQLServerProviderConfigData(pool, configInfo.mjCoreSchema);
await setupSQLServerClient(config);
console.log("Database connection setup completed.");
// Set up routes
setupRoutes();
// Start the server
app.listen(a2aServerPort, () => {
console.log(`MemberJunction A2A Server running on port ${a2aServerPort}`);
console.log(`Agent card available at: http://localhost:${a2aServerPort}/a2a/agent-card`);
});
}
catch (error) {
console.error("Failed to initialize A2A server:", error);
}
}
function setupRoutes() {
// Agent Card endpoint
app.get('/a2a/agent-card', (req, res) => {
const agentCard = generateAgentCard();
res.json(agentCard);
});
// Send a message to a task
app.post('/a2a/tasks/send', (req, res) => {
const result = handleTaskSend(req.body);
res.json(result);
});
// Send a message to a task with streaming response
app.post('/a2a/tasks/sendSubscribe', (req, res) => {
if (!a2aServerSettings?.streamingEnabled) {
return res.status(400).json({
error: {
code: 400,
message: "Streaming is not enabled for this agent"
}
});
}
// Set up SSE connection
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
handleTaskSendSubscribe(req.body, res);
});
// Get a task's status
app.get('/a2a/tasks/:taskId', (req, res) => {
const { taskId } = req.params;
const task = tasks.get(taskId);
if (!task) {
return res.status(404).json({
error: {
code: 404,
message: `Task with ID ${taskId} not found`
}
});
}
res.json(task);
});
// Cancel a task
app.post('/a2a/tasks/:taskId/cancel', (req, res) => {
const { taskId } = req.params;
const task = tasks.get(taskId);
if (!task) {
return res.status(404).json({
error: {
code: 404,
message: `Task with ID ${taskId} not found`
}
});
}
task.status = 'cancelled';
task.updated = new Date();
res.json({ success: true });
});
}
function generateAgentCard() {
const contextUser = UserCache.Instance.Users[0];
const md = new Metadata();
const entityCapabilities = getEntityCapabilities(md.Entities, contextUser);
return {
name: a2aServerSettings?.agentName || "MemberJunction",
description: a2aServerSettings?.agentDescription || "MemberJunction A2A Agent",
version: "1.0.0",
endpoints: {
tasks: `/a2a/tasks`,
agentCard: `/a2a/agent-card`
},
capabilities: {
streaming: !!a2aServerSettings?.streamingEnabled,
asynchronous: false,
multimedia: false,
entities: entityCapabilities
}
};
}
function getEntityCapabilities(allEntities, contextUser) {
const capabilities = [];
const entityCapabilitiesConfig = a2aServerSettings?.entityCapabilities || [];
for (const config of entityCapabilitiesConfig) {
const matchingEntities = getMatchingEntitiesForConfig(allEntities, config);
for (const entity of matchingEntities) {
const operations = [];
if (config.get)
operations.push('get');
if (config.create)
operations.push('create');
if (config.update)
operations.push('update');
if (config.delete)
operations.push('delete');
if (config.runView)
operations.push('query');
if (operations.length > 0) {
capabilities.push({
name: entity.Name,
schema: entity.SchemaName,
operations: operations
});
}
}
}
return capabilities;
}
function getMatchingEntitiesForConfig(allEntities, config) {
return allEntities.filter((entity) => {
const entityName = entity.Name.toLowerCase();
const schemaName = entity.SchemaName.toLowerCase();
const configEntityName = config.entityName?.trim().toLowerCase() || "*";
const configSchemaName = config.schemaName?.trim().toLowerCase() || "*";
let schemaMatch = false;
let entityMatch = false;
// Schema matching
if (configSchemaName === "*") {
schemaMatch = true;
}
else if (configSchemaName.startsWith("*") && configSchemaName.endsWith("*")) {
schemaMatch = schemaName.includes(configSchemaName.slice(1, -1));
}
else if (configSchemaName.endsWith("*")) {
schemaMatch = schemaName.startsWith(configSchemaName.slice(0, -1));
}
else if (configSchemaName.startsWith("*")) {
schemaMatch = schemaName.endsWith(configSchemaName.slice(1));
}
else {
schemaMatch = schemaName === configSchemaName;
}
// Entity matching (only checked if schema matches)
if (schemaMatch) {
if (configEntityName === "*") {
entityMatch = true;
}
else if (configEntityName.startsWith("*") && configEntityName.endsWith("*")) {
entityMatch = entityName.includes(configEntityName.slice(1, -1));
}
else if (configEntityName.endsWith("*")) {
entityMatch = entityName.startsWith(configEntityName.slice(0, -1));
}
else if (configEntityName.startsWith("*")) {
entityMatch = entityName.endsWith(configEntityName.slice(1));
}
else {
entityMatch = entityName === configEntityName;
}
}
return schemaMatch && entityMatch;
});
}
function handleTaskSend(requestBody) {
const { taskId, message } = requestBody;
if (!taskId) {
// Create a new task
const newTaskId = generateId();
const task = {
id: newTaskId,
status: 'pending',
messages: [],
artifacts: [],
created: new Date(),
updated: new Date()
};
if (message) {
const newMessage = {
id: generateId(),
taskId: newTaskId,
role: 'user',
parts: message.parts || [],
created: new Date()
};
task.messages.push(newMessage);
}
tasks.set(newTaskId, task);
// Process the task asynchronously
processTask(task);
return {
taskId: newTaskId,
status: task.status
};
}
else {
// Update an existing task
const task = tasks.get(taskId);
if (!task) {
throw {
error: {
code: 404,
message: `Task with ID ${taskId} not found`
}
};
}
if (task.status === 'completed' || task.status === 'cancelled' || task.status === 'failed') {
throw {
error: {
code: 400,
message: `Cannot update a task with status: ${task.status}`
}
};
}
if (message) {
const newMessage = {
id: generateId(),
taskId,
role: 'user',
parts: message.parts || [],
created: new Date()
};
task.messages.push(newMessage);
task.updated = new Date();
}
// Process the updated task
processTask(task);
return {
taskId: task.id,
status: task.status
};
}
}
function handleTaskSendSubscribe(requestBody, res) {
const result = handleTaskSend(requestBody);
const task = tasks.get(result.taskId);
if (!task) {
res.write(`data: ${JSON.stringify({ error: { code: 404, message: "Task not found" } })}\n\n`);
res.end();
return;
}
// Send initial task state
res.write(`data: ${JSON.stringify({
taskId: task.id,
status: task.status
})}\n\n`);
// Set up a listener for task updates
const updateInterval = setInterval(() => {
const updatedTask = tasks.get(task.id);
if (!updatedTask) {
clearInterval(updateInterval);
res.end();
return;
}
// Send task status updates
res.write(`data: ${JSON.stringify({
taskId: updatedTask.id,
status: updatedTask.status
})}\n\n`);
// If task is completed, send final message and end the stream
if (updatedTask.status === 'completed' || updatedTask.status === 'cancelled' || updatedTask.status === 'failed') {
clearInterval(updateInterval);
// Send final artifacts
if (updatedTask.artifacts.length > 0) {
res.write(`data: ${JSON.stringify({
taskId: updatedTask.id,
artifacts: updatedTask.artifacts
})}\n\n`);
}
res.end();
}
}, 500);
// Handle client disconnect
res.on('close', () => {
clearInterval(updateInterval);
});
}
// Process a task using MemberJunction APIs
async function processTask(task) {
try {
task.status = 'in_progress';
task.updated = new Date();
// Get the last user message
const lastMessage = task.messages.filter(m => m.role === 'user').pop();
if (!lastMessage) {
throw new Error("No user message found");
}
// Initialize entity operations
const entityOps = new EntityOperations();
// Extract text content and parse operation
const textParts = lastMessage.parts.filter(p => p.type === 'text');
const textContent = textParts.map(p => typeof p.content === 'string' ? p.content : JSON.stringify(p.content)).join(" ");
// Extract structured data if any
const dataParts = lastMessage.parts.filter(p => p.type === 'data');
const dataContent = dataParts.length > 0 ? dataParts[0].content : null;
// Parse the command and parameters
let operation = 'unknown';
let entityName = '';
let parameters = {};
// Try to extract command and parameters from structured data first
if (dataContent && typeof dataContent === 'object') {
operation = dataContent.operation || 'unknown';
entityName = dataContent.entity || '';
parameters = dataContent.parameters || {};
}
// Otherwise parse from text
else if (textContent) {
const parsedCommand = entityOps.parseCommandFromText(textContent);
operation = parsedCommand.operation;
entityName = parsedCommand.entityName;
parameters = parsedCommand.parameters;
}
// Perform the operation
let operationResult;
try {
if (!entityName) {
throw new Error("Entity name not specified");
}
operationResult = await entityOps.processOperation(operation, entityName, parameters);
}
catch (error) {
operationResult = {
success: false,
errorMessage: error instanceof Error ? error.message : String(error)
};
}
// Create response message
const responseMessage = {
id: generateId(),
taskId: task.id,
role: 'agent',
parts: [
{
id: generateId(),
type: 'text',
content: operationResult.success
? `Successfully performed ${operation} operation on ${entityName}`
: `Failed to perform ${operation} operation on ${entityName}: ${operationResult.errorMessage}`
}
],
created: new Date()
};
// Create artifact with result data
const artifact = {
id: generateId(),
taskId: task.id,
name: `${operation}_result`,
parts: [
{
id: generateId(),
type: 'data',
content: {
success: operationResult.success,
operation,
entity: entityName,
result: operationResult.result,
error: operationResult.success ? undefined : operationResult.errorMessage,
timestamp: new Date().toISOString()
}
}
],
created: new Date()
};
// Update the task
task.messages.push(responseMessage);
task.artifacts.push(artifact);
task.status = 'completed';
task.updated = new Date();
}
catch (error) {
console.error("Error processing task:", error);
// Create error response
const errorMessage = {
id: generateId(),
taskId: task.id,
role: 'agent',
parts: [
{
id: generateId(),
type: 'text',
content: `An error occurred while processing your request: ${error instanceof Error ? error.message : String(error)}`
}
],
created: new Date()
};
// Create error artifact
const errorArtifact = {
id: generateId(),
taskId: task.id,
name: 'error',
parts: [
{
id: generateId(),
type: 'data',
content: {
success: false,
error: error instanceof Error ? error.message : String(error),
timestamp: new Date().toISOString()
}
}
],
created: new Date()
};
task.messages.push(errorMessage);
task.artifacts.push(errorArtifact);
task.status = 'failed';
task.updated = new Date();
}
}
// Utility function to generate a random ID
function generateId() {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}
//# sourceMappingURL=Server.js.map