@xynehq/jaf
Version:
Juspay Agent Framework - A purely functional agent framework with immutable state and composable tools
394 lines • 16.8 kB
JavaScript
import Fastify from 'fastify';
import cors from '@fastify/cors';
import { chatRequestSchema } from './types';
import { run } from '../core/engine';
import { createRunId, createTraceId } from '../core/types';
import { v4 as uuidv4 } from 'uuid';
/**
* Create and configure a JAF server instance
* Functional implementation following JAF principles
*/
export function createJAFServer(config) {
const startTime = Date.now();
const app = Fastify({
logger: true,
ajv: {
customOptions: {
removeAdditional: false,
useDefaults: true,
coerceTypes: true
}
}
});
const setupMiddleware = async () => {
if (config.cors !== false) {
await app.register(cors, {
origin: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
});
}
// Add request/response validation
app.addHook('preHandler', async (request, reply) => {
// Add CORS headers for preflight requests
if (request.method === 'OPTIONS') {
void reply.header('Access-Control-Allow-Origin', '*');
void reply.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
void reply.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
return reply.code(200).send();
}
});
};
const setupRoutes = () => {
// Health check endpoint
app.get('/health', async (request, reply) => {
const response = {
status: 'healthy',
timestamp: new Date().toISOString(),
version: '2.0.0',
uptime: Date.now() - startTime
};
return reply.code(200).send(response);
});
// List available agents
app.get('/agents', async (request, reply) => {
try {
const agents = Array.from(config.agentRegistry.entries()).map(([name, agent]) => ({
name,
description: typeof agent.instructions === 'function'
? 'Agent description' // Safe fallback since we don't have context
: agent.instructions,
tools: agent.tools?.map(tool => tool.schema.name) || []
}));
const response = {
success: true,
data: { agents }
};
return reply.code(200).send(response);
}
catch (error) {
const response = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
return reply.code(500).send(response);
}
});
// Chat completion endpoint
app.post('/chat', {
schema: {
body: {
type: 'object',
properties: {
messages: {
type: 'array',
items: {
type: 'object',
properties: {
role: { type: 'string', enum: ['user', 'assistant', 'system'] },
content: { type: 'string' }
},
required: ['role', 'content']
}
},
agentName: { type: 'string' },
context: {},
maxTurns: { type: 'number' },
stream: { type: 'boolean', default: false },
conversationId: { type: 'string' },
memory: {
type: 'object',
properties: {
autoStore: { type: 'boolean', default: true },
maxMessages: { type: 'number' },
compressionThreshold: { type: 'number' }
}
}
},
required: ['messages', 'agentName']
}
}
}, async (request, reply) => {
const requestStartTime = Date.now();
try {
// Validate request body
const validatedRequest = chatRequestSchema.parse(request.body);
// Check if agent exists
if (!config.agentRegistry.has(validatedRequest.agentName)) {
const response = {
success: false,
error: `Agent '${validatedRequest.agentName}' not found. Available agents: ${Array.from(config.agentRegistry.keys()).join(', ')}`
};
return reply.code(404).send(response);
}
// Convert HTTP messages to JAF messages
const jafMessages = validatedRequest.messages.map(msg => ({
role: msg.role === 'system' ? 'user' : msg.role,
content: msg.content
}));
// Create initial state
const runId = createRunId(uuidv4());
const traceId = createTraceId(uuidv4());
// Generate conversationId if not provided
const conversationId = validatedRequest.conversationId || `conv-${uuidv4()}`;
const initialState = {
runId,
traceId,
messages: jafMessages,
currentAgentName: validatedRequest.agentName,
context: validatedRequest.context || {},
turnCount: 0
};
// Create run config with memory configuration
const runConfig = {
...config.runConfig,
maxTurns: validatedRequest.maxTurns || config.runConfig.maxTurns || 10,
conversationId,
memory: config.defaultMemoryProvider ? {
provider: config.defaultMemoryProvider,
autoStore: validatedRequest.memory?.autoStore ?? true,
maxMessages: validatedRequest.memory?.maxMessages,
compressionThreshold: validatedRequest.memory?.compressionThreshold
} : undefined
};
// Handle streaming vs non-streaming
if (validatedRequest.stream) {
// For streaming, we'd need to implement Server-Sent Events
// For now, return an error indicating streaming is not yet implemented
const response = {
success: false,
error: 'Streaming is not yet implemented. Set stream: false.'
};
return reply.code(501).send(response);
}
// Run the agent
const result = await run(initialState, runConfig);
const executionTime = Date.now() - requestStartTime;
// Convert JAF messages back to HTTP messages, including tool interactions
const httpMessages = result.finalState.messages.map(msg => {
if (msg.role === 'tool') {
// Include tool messages with special formatting
return {
role: 'tool',
content: msg.content,
tool_call_id: msg.tool_call_id
};
}
else if (msg.role === 'assistant' && msg.tool_calls) {
// Include assistant messages with tool calls
return {
role: msg.role,
content: msg.content || '',
tool_calls: msg.tool_calls.map(tc => ({
id: tc.id,
type: tc.type,
function: {
name: tc.function.name,
arguments: tc.function.arguments
}
}))
};
}
else {
// Regular user/assistant messages
return {
role: msg.role,
content: msg.content
};
}
});
const response = {
success: true,
data: {
runId: result.finalState.runId,
traceId: result.finalState.traceId,
conversationId: conversationId,
messages: httpMessages,
outcome: {
status: result.outcome.status,
output: result.outcome.status === 'completed' ? String(result.outcome.output) : undefined,
error: result.outcome.status === 'error' ? result.outcome.error : undefined
},
turnCount: result.finalState.turnCount,
executionTimeMs: executionTime
}
};
return reply.code(200).send(response);
}
catch (error) {
const executionTime = Date.now() - requestStartTime;
const response = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'
};
request.log.error({
error: error,
executionTimeMs: executionTime,
requestBody: request.body
}, 'Chat endpoint error');
return reply.code(500).send(response);
}
});
// Agent-specific chat endpoint (convenience)
app.post('/agents/:agentName/chat', {
schema: {
params: {
type: 'object',
properties: {
agentName: { type: 'string' }
},
required: ['agentName']
},
body: {
type: 'object',
properties: {
messages: {
type: 'array',
items: {
type: 'object',
properties: {
role: { type: 'string', enum: ['user', 'assistant', 'system'] },
content: { type: 'string' }
},
required: ['role', 'content']
}
},
context: {},
maxTurns: { type: 'number' },
stream: { type: 'boolean', default: false },
conversationId: { type: 'string' },
memory: {
type: 'object',
properties: {
autoStore: { type: 'boolean', default: true },
maxMessages: { type: 'number' },
compressionThreshold: { type: 'number' }
}
}
},
required: ['messages']
}
}
}, async (request, reply) => {
// Delegate to main chat endpoint
const chatRequest = {
...request.body,
agentName: request.params.agentName
};
// Create a new request object for the main handler
const newRequest = {
...request,
body: chatRequest
};
// Call the main chat handler
return app.inject({
method: 'POST',
url: '/chat',
payload: chatRequest
}).then((response) => JSON.parse(response.body));
});
// Memory management endpoints
app.get('/conversations/:conversationId', async (request, reply) => {
if (!config.defaultMemoryProvider) {
return reply.code(503).send({
success: false,
error: 'Memory provider not configured'
});
}
const result = await config.defaultMemoryProvider.getConversation(request.params.conversationId);
if (!result.success) {
return reply.code(500).send({
success: false,
error: result.error.message
});
}
return reply.code(200).send({
success: true,
data: result.data
});
});
app.delete('/conversations/:conversationId', async (request, reply) => {
if (!config.defaultMemoryProvider) {
return reply.code(503).send({
success: false,
error: 'Memory provider not configured'
});
}
const result = await config.defaultMemoryProvider.deleteConversation(request.params.conversationId);
if (!result.success) {
return reply.code(500).send({
success: false,
error: result.error.message
});
}
return reply.code(200).send({
success: true,
data: { deleted: result.data }
});
});
app.get('/memory/health', async (request, reply) => {
if (!config.defaultMemoryProvider) {
return reply.code(503).send({
success: false,
error: 'Memory provider not configured'
});
}
const result = await config.defaultMemoryProvider.healthCheck();
if (!result.success) {
return reply.code(500).send({
success: false,
error: 'Health check failed',
details: result.error
});
}
return reply.code(200).send({
success: true,
data: result.data
});
});
};
const start = async () => {
try {
await setupMiddleware();
setupRoutes();
const host = config.host || 'localhost';
const port = config.port || 3000;
console.log(`🔧 Starting Fastify server on ${host}:${port}...`);
await app.listen({
port,
host
});
console.log(`🔧 Fastify server started successfully`);
console.log(`🚀 JAF Server running on http://${host}:${port}`);
console.log(`📋 Available agents: ${Array.from(config.agentRegistry.keys()).join(', ')}`);
console.log(`🏥 Health check: http://${host}:${port}/health`);
console.log(`🤖 Agents list: http://${host}:${port}/agents`);
console.log(`💬 Chat endpoint: http://${host}:${port}/chat`);
if (config.defaultMemoryProvider) {
console.log(`🧠 Memory provider: Configured`);
console.log(`📊 Memory health: http://${host}:${port}/memory/health`);
console.log(`💾 Conversation management: http://${host}:${port}/conversations/:id`);
}
else {
console.log(`🧠 Memory provider: Not configured (conversations will not persist)`);
}
}
catch (error) {
app.log.error(error);
process.exit(1);
}
};
const stop = async () => {
await app.close();
// Close memory provider if configured
if (config.defaultMemoryProvider) {
await config.defaultMemoryProvider.close();
}
};
return {
app,
start,
stop
};
}
//# sourceMappingURL=server.js.map