Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
34
marketing-agent/services/claude-agent/Dockerfile
Normal file
34
marketing-agent/services/claude-agent/Dockerfile
Normal file
@@ -0,0 +1,34 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install --production
|
||||
|
||||
# Copy app source
|
||||
COPY . .
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup -g 1001 -S nodejs
|
||||
RUN adduser -S nodejs -u 1001
|
||||
|
||||
# Create logs directory with proper permissions
|
||||
RUN mkdir -p logs && chown -R nodejs:nodejs logs
|
||||
USER nodejs
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3002
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \
|
||||
CMD node healthcheck.js || exit 1
|
||||
|
||||
# Start the service
|
||||
CMD ["node", "src/app.js"]
|
||||
29
marketing-agent/services/claude-agent/package.json
Normal file
29
marketing-agent/services/claude-agent/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "claude-agent-service",
|
||||
"version": "1.0.0",
|
||||
"description": "AI-powered decision making service using Claude",
|
||||
"main": "src/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"dev": "nodemon src/index.js",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.20.0",
|
||||
"@hapi/hapi": "^21.3.2",
|
||||
"@hapi/joi": "^17.1.1",
|
||||
"axios": "^1.6.5",
|
||||
"dotenv": "^16.4.1",
|
||||
"ioredis": "^5.3.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"prom-client": "^15.1.0",
|
||||
"uuid": "^9.0.1",
|
||||
"winston": "^3.11.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.3",
|
||||
"jest": "^29.7.0"
|
||||
}
|
||||
}
|
||||
1
marketing-agent/services/claude-agent/src/app.js
Normal file
1
marketing-agent/services/claude-agent/src/app.js
Normal file
@@ -0,0 +1 @@
|
||||
import './index.js';
|
||||
101
marketing-agent/services/claude-agent/src/config/redis.js
Normal file
101
marketing-agent/services/claude-agent/src/config/redis.js
Normal file
@@ -0,0 +1,101 @@
|
||||
import Redis from 'ioredis';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
export class RedisClient {
|
||||
constructor() {
|
||||
this.client = null;
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (!RedisClient.instance) {
|
||||
RedisClient.instance = new RedisClient();
|
||||
}
|
||||
return RedisClient.instance;
|
||||
}
|
||||
|
||||
async connect() {
|
||||
const config = {
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: process.env.REDIS_PORT || 6379,
|
||||
password: process.env.REDIS_PASSWORD || undefined,
|
||||
db: parseInt(process.env.REDIS_DB) || 0,
|
||||
retryStrategy: (times) => {
|
||||
const delay = Math.min(times * 50, 2000);
|
||||
return delay;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
this.client = new Redis(config);
|
||||
|
||||
this.client.on('connect', () => {
|
||||
logger.info('Redis connection established');
|
||||
});
|
||||
|
||||
this.client.on('error', (err) => {
|
||||
logger.error('Redis error:', err);
|
||||
});
|
||||
|
||||
// Wait for connection
|
||||
await this.client.ping();
|
||||
|
||||
return this.client;
|
||||
} catch (error) {
|
||||
logger.error('Failed to connect to Redis:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async checkHealth() {
|
||||
try {
|
||||
const result = await this.client.ping();
|
||||
return result === 'PONG';
|
||||
} catch (error) {
|
||||
logger.error('Redis health check failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
if (this.client) {
|
||||
await this.client.quit();
|
||||
logger.info('Redis connection closed');
|
||||
}
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
async setWithExpiry(key, value, ttl) {
|
||||
return await this.client.setex(key, ttl, JSON.stringify(value));
|
||||
}
|
||||
|
||||
async get(key) {
|
||||
const value = await this.client.get(key);
|
||||
return value ? JSON.parse(value) : null;
|
||||
}
|
||||
|
||||
async del(key) {
|
||||
return await this.client.del(key);
|
||||
}
|
||||
|
||||
async hset(key, field, value) {
|
||||
return await this.client.hset(key, field, JSON.stringify(value));
|
||||
}
|
||||
|
||||
async hget(key, field) {
|
||||
const value = await this.client.hget(key, field);
|
||||
return value ? JSON.parse(value) : null;
|
||||
}
|
||||
|
||||
async hgetall(key) {
|
||||
const data = await this.client.hgetall(key);
|
||||
const result = {};
|
||||
for (const [field, value] of Object.entries(data)) {
|
||||
result[field] = JSON.parse(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async hincrby(key, field, increment) {
|
||||
return await this.client.hincrby(key, field, increment);
|
||||
}
|
||||
}
|
||||
103
marketing-agent/services/claude-agent/src/index.js
Normal file
103
marketing-agent/services/claude-agent/src/index.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import Hapi from '@hapi/hapi';
|
||||
import dotenv from 'dotenv';
|
||||
import { logger } from './utils/logger.js';
|
||||
import { ClaudeService } from './services/ClaudeService.js';
|
||||
import { PromptManager } from './services/PromptManager.js';
|
||||
import { ContextManager } from './services/ContextManager.js';
|
||||
import { FunctionRegistry } from './services/FunctionRegistry.js';
|
||||
import { RedisClient } from './config/redis.js';
|
||||
import routes from './routes/index.js';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
const init = async () => {
|
||||
// Initialize Redis
|
||||
await RedisClient.getInstance().connect();
|
||||
|
||||
// Initialize services
|
||||
const claudeService = ClaudeService.getInstance();
|
||||
const promptManager = PromptManager.getInstance();
|
||||
const contextManager = ContextManager.getInstance();
|
||||
const functionRegistry = FunctionRegistry.getInstance();
|
||||
|
||||
// Initialize function registry
|
||||
await functionRegistry.initialize();
|
||||
|
||||
// Create Hapi server
|
||||
const server = Hapi.server({
|
||||
port: process.env.PORT || 3002,
|
||||
host: '0.0.0.0',
|
||||
routes: {
|
||||
cors: {
|
||||
origin: ['*'],
|
||||
headers: ['Accept', 'Content-Type', 'Authorization'],
|
||||
credentials: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Register routes
|
||||
server.route(routes);
|
||||
|
||||
// Health check endpoint
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/health',
|
||||
options: {
|
||||
auth: false,
|
||||
handler: async (request, h) => {
|
||||
const redisHealth = await RedisClient.getInstance().checkHealth();
|
||||
const claudeHealth = await claudeService.checkHealth();
|
||||
|
||||
const isHealthy = redisHealth && claudeHealth;
|
||||
|
||||
return h.response({
|
||||
status: isHealthy ? 'healthy' : 'unhealthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
services: {
|
||||
redis: redisHealth ? 'up' : 'down',
|
||||
claude: claudeHealth ? 'up' : 'down'
|
||||
}
|
||||
}).code(isHealthy ? 200 : 503);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Metrics endpoint
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/metrics',
|
||||
options: {
|
||||
auth: false,
|
||||
handler: async (request, h) => {
|
||||
const metrics = await claudeService.getMetrics();
|
||||
return h.response(metrics).type('text/plain');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Start server
|
||||
await server.start();
|
||||
logger.info(`Claude Agent service started on ${server.info.uri}`);
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
logger.info('Shutting down gracefully...');
|
||||
await server.stop();
|
||||
await RedisClient.getInstance().disconnect();
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
|
||||
// Handle uncaught errors
|
||||
process.on('unhandledRejection', (err) => {
|
||||
logger.error('Unhandled rejection:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Start the service
|
||||
init().catch((err) => {
|
||||
logger.error('Failed to start service:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import Joi from '@hapi/joi';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
/**
|
||||
* Validation middleware for Hapi routes
|
||||
*/
|
||||
export function validateRequest(schema) {
|
||||
return {
|
||||
validate: {
|
||||
payload: schema.payload,
|
||||
params: schema.params,
|
||||
query: schema.query,
|
||||
headers: schema.headers,
|
||||
failAction: (request, h, err) => {
|
||||
logger.warn('Validation error', {
|
||||
error: err.message,
|
||||
path: request.path,
|
||||
method: request.method
|
||||
});
|
||||
|
||||
const error = {
|
||||
success: false,
|
||||
error: 'Validation failed',
|
||||
details: err.details.map(detail => ({
|
||||
field: detail.path.join('.'),
|
||||
message: detail.message
|
||||
}))
|
||||
};
|
||||
|
||||
return h.response(error).code(400).takeover();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Common validation schemas
|
||||
*/
|
||||
export const schemas = {
|
||||
id: Joi.string().uuid(),
|
||||
sessionId: Joi.string().required(),
|
||||
pagination: {
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20)
|
||||
},
|
||||
dateRange: {
|
||||
startDate: Joi.date().iso(),
|
||||
endDate: Joi.date().iso().greater(Joi.ref('startDate'))
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validator builders
|
||||
*/
|
||||
export const validators = {
|
||||
body: (schema) => ({ payload: schema }),
|
||||
params: (schema) => ({ params: schema }),
|
||||
query: (schema) => ({ query: schema }),
|
||||
headers: (schema) => ({ headers: schema })
|
||||
};
|
||||
375
marketing-agent/services/claude-agent/src/routes/index.js
Normal file
375
marketing-agent/services/claude-agent/src/routes/index.js
Normal file
@@ -0,0 +1,375 @@
|
||||
import Joi from '@hapi/joi';
|
||||
import { ClaudeService } from '../services/ClaudeService.js';
|
||||
import { ContextManager } from '../services/ContextManager.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { intelligenceRoutes } from './intelligenceHapi.js';
|
||||
|
||||
const claudeService = ClaudeService.getInstance();
|
||||
const contextManager = ContextManager.getInstance();
|
||||
|
||||
export default [
|
||||
// Generate campaign strategy
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/v1/campaign/strategy',
|
||||
options: {
|
||||
validate: {
|
||||
payload: Joi.object({
|
||||
sessionId: Joi.string().required(),
|
||||
campaignData: Joi.object({
|
||||
goals: Joi.array().items(Joi.string()).required(),
|
||||
targetAudience: Joi.object().required(),
|
||||
budget: Joi.number().positive().required(),
|
||||
duration: Joi.string().required()
|
||||
}).required()
|
||||
})
|
||||
},
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const { sessionId, campaignData } = request.payload;
|
||||
|
||||
// Add to context
|
||||
await contextManager.addMessage(
|
||||
sessionId,
|
||||
'user',
|
||||
`Generate campaign strategy with parameters: ${JSON.stringify(campaignData)}`
|
||||
);
|
||||
|
||||
// Generate strategy
|
||||
const strategy = await claudeService.generateCampaignStrategy(campaignData);
|
||||
|
||||
// Add response to context
|
||||
await contextManager.addMessage(
|
||||
sessionId,
|
||||
'assistant',
|
||||
JSON.stringify(strategy),
|
||||
{ type: 'campaign_strategy' }
|
||||
);
|
||||
|
||||
return h.response({
|
||||
success: true,
|
||||
data: strategy
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to generate campaign strategy:', error);
|
||||
return h.response({
|
||||
success: false,
|
||||
error: error.message
|
||||
}).code(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Analyze message
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/v1/message/analyze',
|
||||
options: {
|
||||
validate: {
|
||||
payload: Joi.object({
|
||||
sessionId: Joi.string().required(),
|
||||
messageData: Joi.object({
|
||||
content: Joi.string().required(),
|
||||
context: Joi.object().default({}),
|
||||
intent: Joi.string().optional()
|
||||
}).required()
|
||||
})
|
||||
},
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const { sessionId, messageData } = request.payload;
|
||||
|
||||
// Add to context
|
||||
await contextManager.addMessage(
|
||||
sessionId,
|
||||
'user',
|
||||
`Analyze message: ${messageData.content}`
|
||||
);
|
||||
|
||||
// Analyze message
|
||||
const analysis = await claudeService.analyzeMessage(messageData);
|
||||
|
||||
// Add response to context
|
||||
await contextManager.addMessage(
|
||||
sessionId,
|
||||
'assistant',
|
||||
JSON.stringify(analysis),
|
||||
{ type: 'message_analysis' }
|
||||
);
|
||||
|
||||
return h.response({
|
||||
success: true,
|
||||
data: analysis
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to analyze message:', error);
|
||||
return h.response({
|
||||
success: false,
|
||||
error: error.message
|
||||
}).code(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Optimize content
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/v1/content/optimize',
|
||||
options: {
|
||||
validate: {
|
||||
payload: Joi.object({
|
||||
sessionId: Joi.string().required(),
|
||||
content: Joi.string().required(),
|
||||
criteria: Joi.object({
|
||||
platform: Joi.string().valid('telegram', 'whatsapp', 'facebook').default('telegram'),
|
||||
objective: Joi.string().valid('engagement', 'conversion', 'awareness').default('engagement'),
|
||||
tone: Joi.string().valid('formal', 'casual', 'friendly', 'professional').optional(),
|
||||
length: Joi.string().valid('short', 'medium', 'long').optional()
|
||||
}).default({})
|
||||
})
|
||||
},
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const { sessionId, content, criteria } = request.payload;
|
||||
|
||||
// Add to context
|
||||
await contextManager.addMessage(
|
||||
sessionId,
|
||||
'user',
|
||||
`Optimize content: ${content.substring(0, 100)}...`
|
||||
);
|
||||
|
||||
// Optimize content
|
||||
const optimized = await claudeService.optimizeContent(content, criteria);
|
||||
|
||||
// Add response to context
|
||||
await contextManager.addMessage(
|
||||
sessionId,
|
||||
'assistant',
|
||||
optimized,
|
||||
{ type: 'content_optimization' }
|
||||
);
|
||||
|
||||
return h.response({
|
||||
success: true,
|
||||
data: {
|
||||
original: content,
|
||||
optimized: optimized,
|
||||
criteria: criteria
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to optimize content:', error);
|
||||
return h.response({
|
||||
success: false,
|
||||
error: error.message
|
||||
}).code(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Chat with Claude (general purpose)
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/v1/chat',
|
||||
options: {
|
||||
validate: {
|
||||
payload: Joi.object({
|
||||
sessionId: Joi.string().required(),
|
||||
message: Joi.string().required(),
|
||||
functions: Joi.array().items(Joi.string()).optional(),
|
||||
temperature: Joi.number().min(0).max(1).optional(),
|
||||
maxTokens: Joi.number().positive().optional()
|
||||
})
|
||||
},
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const { sessionId, message, functions, temperature, maxTokens } = request.payload;
|
||||
|
||||
// Get conversation history
|
||||
const history = await contextManager.getConversationHistory(sessionId, 10);
|
||||
|
||||
// Add user message to context
|
||||
await contextManager.addMessage(sessionId, 'user', message);
|
||||
|
||||
// Prepare messages for Claude
|
||||
const messages = [
|
||||
...history.map(msg => ({
|
||||
role: msg.role,
|
||||
content: msg.content
|
||||
})),
|
||||
{
|
||||
role: 'user',
|
||||
content: message
|
||||
}
|
||||
];
|
||||
|
||||
// Call Claude
|
||||
const response = await claudeService.callClaude({
|
||||
messages,
|
||||
functions: functions || [],
|
||||
temperature,
|
||||
maxTokens
|
||||
});
|
||||
|
||||
// Add response to context
|
||||
await contextManager.addMessage(
|
||||
sessionId,
|
||||
'assistant',
|
||||
response.content,
|
||||
{
|
||||
functions_called: response.function_calls,
|
||||
usage: response.usage
|
||||
}
|
||||
);
|
||||
|
||||
return h.response({
|
||||
success: true,
|
||||
data: response
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Chat failed:', error);
|
||||
return h.response({
|
||||
success: false,
|
||||
error: error.message
|
||||
}).code(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Get context/conversation history
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/v1/context/{sessionId}',
|
||||
options: {
|
||||
validate: {
|
||||
params: Joi.object({
|
||||
sessionId: Joi.string().required()
|
||||
}),
|
||||
query: Joi.object({
|
||||
limit: Joi.number().positive().default(20)
|
||||
})
|
||||
},
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const { sessionId } = request.params;
|
||||
const { limit } = request.query;
|
||||
|
||||
const context = await contextManager.getContext(sessionId);
|
||||
const history = await contextManager.getConversationHistory(sessionId, limit);
|
||||
|
||||
return h.response({
|
||||
success: true,
|
||||
data: {
|
||||
sessionId,
|
||||
metadata: context.metadata,
|
||||
messageCount: context.messages.length,
|
||||
tokenCount: context.tokenCount,
|
||||
history
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get context:', error);
|
||||
return h.response({
|
||||
success: false,
|
||||
error: error.message
|
||||
}).code(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Clear context
|
||||
{
|
||||
method: 'DELETE',
|
||||
path: '/api/v1/context/{sessionId}',
|
||||
options: {
|
||||
validate: {
|
||||
params: Joi.object({
|
||||
sessionId: Joi.string().required()
|
||||
})
|
||||
},
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const { sessionId } = request.params;
|
||||
|
||||
await contextManager.clearContext(sessionId);
|
||||
|
||||
return h.response({
|
||||
success: true,
|
||||
message: 'Context cleared successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to clear context:', error);
|
||||
return h.response({
|
||||
success: false,
|
||||
error: error.message
|
||||
}).code(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Get available functions
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/v1/functions',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const functionRegistry = require('../services/FunctionRegistry.js').FunctionRegistry.getInstance();
|
||||
const functions = functionRegistry.getAllFunctions();
|
||||
const schemas = functions.map(name => functionRegistry.getFunctionSchema(name));
|
||||
|
||||
return h.response({
|
||||
success: true,
|
||||
data: schemas
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get functions:', error);
|
||||
return h.response({
|
||||
success: false,
|
||||
error: error.message
|
||||
}).code(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Get usage statistics
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/v1/stats/usage',
|
||||
options: {
|
||||
validate: {
|
||||
query: Joi.object({
|
||||
timeRange: Joi.string().valid('1h', '24h', '7d', '30d').default('24h')
|
||||
})
|
||||
},
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const { timeRange } = request.query;
|
||||
const stats = await claudeService.getUsageStats(timeRange);
|
||||
|
||||
return h.response({
|
||||
success: true,
|
||||
data: stats
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get usage stats:', error);
|
||||
return h.response({
|
||||
success: false,
|
||||
error: error.message
|
||||
}).code(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Include all intelligence routes
|
||||
...intelligenceRoutes
|
||||
];
|
||||
360
marketing-agent/services/claude-agent/src/routes/intelligence.js
Normal file
360
marketing-agent/services/claude-agent/src/routes/intelligence.js
Normal file
@@ -0,0 +1,360 @@
|
||||
import express from 'express';
|
||||
import { marketingIntelligence } from '../services/MarketingIntelligence.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { validateRequest } from '../middleware/validation.js';
|
||||
import { body, param } from 'express-validator';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* @route POST /api/intelligence/analyze-campaign
|
||||
* @desc Analyze campaign and provide AI-powered suggestions
|
||||
* @access Private
|
||||
*/
|
||||
router.post('/analyze-campaign',
|
||||
validateRequest([
|
||||
body('campaignId').notEmpty().isString(),
|
||||
body('campaignData').isObject(),
|
||||
body('campaignData.name').notEmpty().isString(),
|
||||
body('campaignData.targetAudience').isObject(),
|
||||
body('campaignData.messageContent').notEmpty().isString(),
|
||||
body('campaignData.metrics').isObject(),
|
||||
body('includeHistorical').optional().isBoolean()
|
||||
]),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { campaignId, campaignData, includeHistorical } = req.body;
|
||||
|
||||
// Check cache first
|
||||
const cached = await marketingIntelligence.getCachedAnalysis(campaignId);
|
||||
if (cached && !includeHistorical) {
|
||||
logger.info('Returning cached campaign analysis', { campaignId });
|
||||
return res.json({
|
||||
success: true,
|
||||
data: cached.analysis,
|
||||
cached: true,
|
||||
timestamp: cached.timestamp
|
||||
});
|
||||
}
|
||||
|
||||
// Perform AI analysis
|
||||
const analysis = await marketingIntelligence.analyzeCampaign(campaignData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: analysis,
|
||||
cached: false,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
logger.info('Campaign analysis completed', {
|
||||
campaignId,
|
||||
score: analysis.summary.score
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Campaign analysis failed:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to analyze campaign',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @route POST /api/intelligence/generate-message
|
||||
* @desc Generate optimized marketing messages
|
||||
* @access Private
|
||||
*/
|
||||
router.post('/generate-message',
|
||||
validateRequest([
|
||||
body('product').notEmpty().isString(),
|
||||
body('targetAudience').notEmpty().isString(),
|
||||
body('goal').notEmpty().isString(),
|
||||
body('tone').optional().isString(),
|
||||
body('features').optional().isArray(),
|
||||
body('cta').optional().isString(),
|
||||
body('characterLimit').optional().isInt({ min: 50, max: 4096 })
|
||||
]),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const context = {
|
||||
product: req.body.product,
|
||||
targetAudience: req.body.targetAudience,
|
||||
goal: req.body.goal,
|
||||
tone: req.body.tone || 'professional',
|
||||
features: req.body.features || [],
|
||||
cta: req.body.cta || 'Learn more',
|
||||
characterLimit: req.body.characterLimit,
|
||||
additionalInfo: req.body.additionalInfo
|
||||
};
|
||||
|
||||
const suggestions = await marketingIntelligence.generateMessageContent(context);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
variations: suggestions,
|
||||
context,
|
||||
generatedAt: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('Messages generated', {
|
||||
count: suggestions.length,
|
||||
product: context.product
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Message generation failed:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to generate messages',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @route POST /api/intelligence/segment-audience
|
||||
* @desc Suggest audience segments based on user data
|
||||
* @access Private
|
||||
*/
|
||||
router.post('/segment-audience',
|
||||
validateRequest([
|
||||
body('totalUsers').isInt({ min: 1 }),
|
||||
body('demographics').isObject(),
|
||||
body('behavioral').isObject(),
|
||||
body('engagementHistory').optional().isObject()
|
||||
]),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const userData = {
|
||||
totalUsers: req.body.totalUsers,
|
||||
demographics: req.body.demographics,
|
||||
behavioral: req.body.behavioral,
|
||||
engagementHistory: req.body.engagementHistory || {}
|
||||
};
|
||||
|
||||
const segments = await marketingIntelligence.suggestAudienceSegments(userData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
segments,
|
||||
totalSegments: segments.length,
|
||||
totalUsers: userData.totalUsers,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('Audience segments generated', {
|
||||
segmentCount: segments.length,
|
||||
totalUsers: userData.totalUsers
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Audience segmentation failed:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to generate audience segments',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @route POST /api/intelligence/optimize-timing
|
||||
* @desc Get optimal timing recommendations
|
||||
* @access Private
|
||||
*/
|
||||
router.post('/optimize-timing',
|
||||
validateRequest([
|
||||
body('sendTimes').isArray(),
|
||||
body('activityPatterns').isObject(),
|
||||
body('timeZones').isObject()
|
||||
]),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const historicalData = {
|
||||
sendTimes: req.body.sendTimes,
|
||||
activityPatterns: req.body.activityPatterns,
|
||||
timeZones: req.body.timeZones
|
||||
};
|
||||
|
||||
const recommendations = await marketingIntelligence.optimizeTiming(historicalData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: recommendations
|
||||
});
|
||||
|
||||
logger.info('Timing optimization completed', {
|
||||
optimalWindows: recommendations.optimalWindows.length
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Timing optimization failed:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to optimize timing',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @route POST /api/intelligence/ab-test-recommendations
|
||||
* @desc Generate A/B test recommendations
|
||||
* @access Private
|
||||
*/
|
||||
router.post('/ab-test-recommendations',
|
||||
validateRequest([
|
||||
body('type').notEmpty().isString(),
|
||||
body('currentMessage').notEmpty().isString(),
|
||||
body('targetMetrics').isArray(),
|
||||
body('audienceSize').isInt({ min: 100 }),
|
||||
body('duration').notEmpty().isString()
|
||||
]),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const campaignContext = {
|
||||
type: req.body.type,
|
||||
currentMessage: req.body.currentMessage,
|
||||
targetMetrics: req.body.targetMetrics,
|
||||
audienceSize: req.body.audienceSize,
|
||||
duration: req.body.duration
|
||||
};
|
||||
|
||||
const recommendations = await marketingIntelligence.generateABTestRecommendations(campaignContext);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: recommendations
|
||||
});
|
||||
|
||||
logger.info('A/B test recommendations generated', {
|
||||
testCount: recommendations.tests.length,
|
||||
audienceSize: campaignContext.audienceSize
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('A/B test generation failed:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to generate A/B test recommendations',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @route POST /api/intelligence/check-compliance
|
||||
* @desc Check message compliance with regulations
|
||||
* @access Private
|
||||
*/
|
||||
router.post('/check-compliance',
|
||||
validateRequest([
|
||||
body('messageContent').notEmpty().isString(),
|
||||
body('targetRegions').isArray().notEmpty()
|
||||
]),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { messageContent, targetRegions } = req.body;
|
||||
|
||||
const complianceCheck = await marketingIntelligence.checkCompliance(
|
||||
messageContent,
|
||||
targetRegions
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: complianceCheck
|
||||
});
|
||||
|
||||
logger.info('Compliance check completed', {
|
||||
score: complianceCheck.score,
|
||||
regions: targetRegions.length
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Compliance check failed:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to check compliance',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @route POST /api/intelligence/engagement-strategies
|
||||
* @desc Get engagement improvement strategies
|
||||
* @access Private
|
||||
*/
|
||||
router.post('/engagement-strategies',
|
||||
validateRequest([
|
||||
body('current').isObject(),
|
||||
body('trends').isObject(),
|
||||
body('feedback').optional().isObject()
|
||||
]),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const campaignMetrics = {
|
||||
current: req.body.current,
|
||||
trends: req.body.trends,
|
||||
feedback: req.body.feedback || {}
|
||||
};
|
||||
|
||||
const strategies = await marketingIntelligence.suggestEngagementStrategies(campaignMetrics);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: strategies
|
||||
});
|
||||
|
||||
logger.info('Engagement strategies generated', {
|
||||
quickWins: strategies.quickWins.length
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Engagement strategy generation failed:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to generate engagement strategies',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @route GET /api/intelligence/health
|
||||
* @desc Check AI service health
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/health', async (req, res) => {
|
||||
try {
|
||||
// Simple health check
|
||||
const health = {
|
||||
status: 'healthy',
|
||||
service: 'marketing-intelligence',
|
||||
timestamp: new Date().toISOString(),
|
||||
apiKeyConfigured: !!process.env.ANTHROPIC_API_KEY
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: health
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Health check failed:', error);
|
||||
res.status(503).json({
|
||||
success: false,
|
||||
error: 'Service unhealthy',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,374 @@
|
||||
import Joi from '@hapi/joi';
|
||||
import { marketingIntelligence } from '../services/MarketingIntelligence.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
export const intelligenceRoutes = [
|
||||
// Analyze campaign
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/intelligence/analyze-campaign',
|
||||
options: {
|
||||
validate: {
|
||||
payload: Joi.object({
|
||||
campaignId: Joi.string().required(),
|
||||
campaignData: Joi.object({
|
||||
name: Joi.string().required(),
|
||||
targetAudience: Joi.object().required(),
|
||||
messageContent: Joi.string().required(),
|
||||
metrics: Joi.object().required(),
|
||||
historicalMetrics: Joi.object().optional()
|
||||
}).required(),
|
||||
includeHistorical: Joi.boolean().optional()
|
||||
})
|
||||
},
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const { campaignId, campaignData, includeHistorical } = request.payload;
|
||||
|
||||
// Check cache first
|
||||
const cached = await marketingIntelligence.getCachedAnalysis(campaignId);
|
||||
if (cached && !includeHistorical) {
|
||||
logger.info('Returning cached campaign analysis', { campaignId });
|
||||
return h.response({
|
||||
success: true,
|
||||
data: cached.analysis,
|
||||
cached: true,
|
||||
timestamp: cached.timestamp
|
||||
});
|
||||
}
|
||||
|
||||
// Perform AI analysis
|
||||
const analysis = await marketingIntelligence.analyzeCampaign(campaignData);
|
||||
|
||||
logger.info('Campaign analysis completed', {
|
||||
campaignId,
|
||||
score: analysis.summary.score
|
||||
});
|
||||
|
||||
return h.response({
|
||||
success: true,
|
||||
data: analysis,
|
||||
cached: false,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Campaign analysis failed:', error);
|
||||
return h.response({
|
||||
success: false,
|
||||
error: 'Failed to analyze campaign',
|
||||
message: error.message
|
||||
}).code(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Generate message
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/intelligence/generate-message',
|
||||
options: {
|
||||
validate: {
|
||||
payload: Joi.object({
|
||||
product: Joi.string().required(),
|
||||
targetAudience: Joi.string().required(),
|
||||
goal: Joi.string().required(),
|
||||
tone: Joi.string().optional(),
|
||||
features: Joi.array().items(Joi.string()).optional(),
|
||||
cta: Joi.string().optional(),
|
||||
characterLimit: Joi.number().min(50).max(4096).optional(),
|
||||
additionalInfo: Joi.string().optional()
|
||||
})
|
||||
},
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const context = {
|
||||
product: request.payload.product,
|
||||
targetAudience: request.payload.targetAudience,
|
||||
goal: request.payload.goal,
|
||||
tone: request.payload.tone || 'professional',
|
||||
features: request.payload.features || [],
|
||||
cta: request.payload.cta || 'Learn more',
|
||||
characterLimit: request.payload.characterLimit,
|
||||
additionalInfo: request.payload.additionalInfo
|
||||
};
|
||||
|
||||
const suggestions = await marketingIntelligence.generateMessageContent(context);
|
||||
|
||||
logger.info('Messages generated', {
|
||||
count: suggestions.length,
|
||||
product: context.product
|
||||
});
|
||||
|
||||
return h.response({
|
||||
success: true,
|
||||
data: {
|
||||
variations: suggestions,
|
||||
context,
|
||||
generatedAt: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Message generation failed:', error);
|
||||
return h.response({
|
||||
success: false,
|
||||
error: 'Failed to generate messages',
|
||||
message: error.message
|
||||
}).code(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Segment audience
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/intelligence/segment-audience',
|
||||
options: {
|
||||
validate: {
|
||||
payload: Joi.object({
|
||||
totalUsers: Joi.number().integer().min(1).required(),
|
||||
demographics: Joi.object().required(),
|
||||
behavioral: Joi.object().required(),
|
||||
engagementHistory: Joi.object().optional()
|
||||
})
|
||||
},
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const userData = {
|
||||
totalUsers: request.payload.totalUsers,
|
||||
demographics: request.payload.demographics,
|
||||
behavioral: request.payload.behavioral,
|
||||
engagementHistory: request.payload.engagementHistory || {}
|
||||
};
|
||||
|
||||
const segments = await marketingIntelligence.suggestAudienceSegments(userData);
|
||||
|
||||
logger.info('Audience segments generated', {
|
||||
segmentCount: segments.length,
|
||||
totalUsers: userData.totalUsers
|
||||
});
|
||||
|
||||
return h.response({
|
||||
success: true,
|
||||
data: {
|
||||
segments,
|
||||
totalSegments: segments.length,
|
||||
totalUsers: userData.totalUsers,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Audience segmentation failed:', error);
|
||||
return h.response({
|
||||
success: false,
|
||||
error: 'Failed to generate audience segments',
|
||||
message: error.message
|
||||
}).code(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Optimize timing
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/intelligence/optimize-timing',
|
||||
options: {
|
||||
validate: {
|
||||
payload: Joi.object({
|
||||
sendTimes: Joi.array().required(),
|
||||
activityPatterns: Joi.object().required(),
|
||||
timeZones: Joi.object().required()
|
||||
})
|
||||
},
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const historicalData = {
|
||||
sendTimes: request.payload.sendTimes,
|
||||
activityPatterns: request.payload.activityPatterns,
|
||||
timeZones: request.payload.timeZones
|
||||
};
|
||||
|
||||
const recommendations = await marketingIntelligence.optimizeTiming(historicalData);
|
||||
|
||||
logger.info('Timing optimization completed', {
|
||||
optimalWindows: recommendations.optimalWindows.length
|
||||
});
|
||||
|
||||
return h.response({
|
||||
success: true,
|
||||
data: recommendations
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Timing optimization failed:', error);
|
||||
return h.response({
|
||||
success: false,
|
||||
error: 'Failed to optimize timing',
|
||||
message: error.message
|
||||
}).code(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// A/B test recommendations
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/intelligence/ab-test-recommendations',
|
||||
options: {
|
||||
validate: {
|
||||
payload: Joi.object({
|
||||
type: Joi.string().required(),
|
||||
currentMessage: Joi.string().required(),
|
||||
targetMetrics: Joi.array().items(Joi.string()).required(),
|
||||
audienceSize: Joi.number().integer().min(100).required(),
|
||||
duration: Joi.string().required()
|
||||
})
|
||||
},
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const campaignContext = {
|
||||
type: request.payload.type,
|
||||
currentMessage: request.payload.currentMessage,
|
||||
targetMetrics: request.payload.targetMetrics,
|
||||
audienceSize: request.payload.audienceSize,
|
||||
duration: request.payload.duration
|
||||
};
|
||||
|
||||
const recommendations = await marketingIntelligence.generateABTestRecommendations(campaignContext);
|
||||
|
||||
logger.info('A/B test recommendations generated', {
|
||||
testCount: recommendations.tests.length,
|
||||
audienceSize: campaignContext.audienceSize
|
||||
});
|
||||
|
||||
return h.response({
|
||||
success: true,
|
||||
data: recommendations
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('A/B test generation failed:', error);
|
||||
return h.response({
|
||||
success: false,
|
||||
error: 'Failed to generate A/B test recommendations',
|
||||
message: error.message
|
||||
}).code(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Check compliance
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/intelligence/check-compliance',
|
||||
options: {
|
||||
validate: {
|
||||
payload: Joi.object({
|
||||
messageContent: Joi.string().required(),
|
||||
targetRegions: Joi.array().items(Joi.string()).min(1).required()
|
||||
})
|
||||
},
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const { messageContent, targetRegions } = request.payload;
|
||||
|
||||
const complianceCheck = await marketingIntelligence.checkCompliance(
|
||||
messageContent,
|
||||
targetRegions
|
||||
);
|
||||
|
||||
logger.info('Compliance check completed', {
|
||||
score: complianceCheck.score,
|
||||
regions: targetRegions.length
|
||||
});
|
||||
|
||||
return h.response({
|
||||
success: true,
|
||||
data: complianceCheck
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Compliance check failed:', error);
|
||||
return h.response({
|
||||
success: false,
|
||||
error: 'Failed to check compliance',
|
||||
message: error.message
|
||||
}).code(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Engagement strategies
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/intelligence/engagement-strategies',
|
||||
options: {
|
||||
validate: {
|
||||
payload: Joi.object({
|
||||
current: Joi.object().required(),
|
||||
trends: Joi.object().required(),
|
||||
feedback: Joi.object().optional()
|
||||
})
|
||||
},
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const campaignMetrics = {
|
||||
current: request.payload.current,
|
||||
trends: request.payload.trends,
|
||||
feedback: request.payload.feedback || {}
|
||||
};
|
||||
|
||||
const strategies = await marketingIntelligence.suggestEngagementStrategies(campaignMetrics);
|
||||
|
||||
logger.info('Engagement strategies generated', {
|
||||
quickWins: strategies.quickWins.length
|
||||
});
|
||||
|
||||
return h.response({
|
||||
success: true,
|
||||
data: strategies
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Engagement strategy generation failed:', error);
|
||||
return h.response({
|
||||
success: false,
|
||||
error: 'Failed to generate engagement strategies',
|
||||
message: error.message
|
||||
}).code(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Health check
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/intelligence/health',
|
||||
options: {
|
||||
handler: async (request, h) => {
|
||||
try {
|
||||
const health = {
|
||||
status: 'healthy',
|
||||
service: 'marketing-intelligence',
|
||||
timestamp: new Date().toISOString(),
|
||||
apiKeyConfigured: !!process.env.ANTHROPIC_API_KEY
|
||||
};
|
||||
|
||||
return h.response({
|
||||
success: true,
|
||||
data: health
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Health check failed:', error);
|
||||
return h.response({
|
||||
success: false,
|
||||
error: 'Service unhealthy',
|
||||
message: error.message
|
||||
}).code(503);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,296 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { RedisClient } from '../config/redis.js';
|
||||
import { PromptManager } from './PromptManager.js';
|
||||
import { FunctionRegistry } from './FunctionRegistry.js';
|
||||
import * as promClient from 'prom-client';
|
||||
|
||||
export class ClaudeService {
|
||||
constructor() {
|
||||
this.client = null;
|
||||
this.redis = null;
|
||||
this.promptManager = null;
|
||||
this.functionRegistry = null;
|
||||
|
||||
// Initialize metrics
|
||||
this.metrics = {
|
||||
apiCalls: new promClient.Counter({
|
||||
name: 'claude_api_calls_total',
|
||||
help: 'Total number of Claude API calls',
|
||||
labelNames: ['status', 'function']
|
||||
}),
|
||||
apiLatency: new promClient.Histogram({
|
||||
name: 'claude_api_latency_seconds',
|
||||
help: 'Claude API call latency',
|
||||
buckets: [0.1, 0.5, 1, 2, 5, 10]
|
||||
}),
|
||||
tokenUsage: new promClient.Counter({
|
||||
name: 'claude_token_usage_total',
|
||||
help: 'Total tokens used',
|
||||
labelNames: ['type'] // input, output
|
||||
}),
|
||||
cacheHits: new promClient.Counter({
|
||||
name: 'claude_cache_hits_total',
|
||||
help: 'Number of cache hits'
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (!ClaudeService.instance) {
|
||||
ClaudeService.instance = new ClaudeService();
|
||||
ClaudeService.instance.initialize();
|
||||
}
|
||||
return ClaudeService.instance;
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.client = new Anthropic({
|
||||
apiKey: process.env.CLAUDE_API_KEY
|
||||
});
|
||||
|
||||
this.redis = RedisClient.getInstance();
|
||||
this.promptManager = PromptManager.getInstance();
|
||||
this.functionRegistry = FunctionRegistry.getInstance();
|
||||
|
||||
logger.info('Claude service initialized');
|
||||
}
|
||||
|
||||
async generateCampaignStrategy(campaignData) {
|
||||
const { goals, targetAudience, budget, duration } = campaignData;
|
||||
|
||||
try {
|
||||
const prompt = await this.promptManager.getPrompt('campaign_strategy', {
|
||||
goals,
|
||||
targetAudience,
|
||||
budget,
|
||||
duration
|
||||
});
|
||||
|
||||
const response = await this.callClaude({
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt
|
||||
}
|
||||
],
|
||||
functions: [
|
||||
'analyze_audience',
|
||||
'generate_content_ideas',
|
||||
'plan_schedule',
|
||||
'estimate_results'
|
||||
]
|
||||
});
|
||||
|
||||
return {
|
||||
strategy: response.content,
|
||||
functions: response.function_calls || []
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to generate campaign strategy:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async analyzeMessage(messageData) {
|
||||
const { content, context, intent } = messageData;
|
||||
|
||||
try {
|
||||
const prompt = await this.promptManager.getPrompt('message_analysis', {
|
||||
content,
|
||||
context,
|
||||
intent
|
||||
});
|
||||
|
||||
const response = await this.callClaude({
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt
|
||||
}
|
||||
],
|
||||
functions: [
|
||||
'check_content_safety',
|
||||
'analyze_sentiment',
|
||||
'extract_entities'
|
||||
]
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
logger.error('Failed to analyze message:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async optimizeContent(content, criteria) {
|
||||
try {
|
||||
const prompt = await this.promptManager.getPrompt('content_optimization', {
|
||||
content,
|
||||
criteria
|
||||
});
|
||||
|
||||
const response = await this.callClaude({
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return response.content;
|
||||
} catch (error) {
|
||||
logger.error('Failed to optimize content:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async callClaude(options) {
|
||||
const { messages, functions = [], maxTokens = 4000, temperature = 0.7 } = options;
|
||||
|
||||
// Check cache
|
||||
const cacheKey = this.generateCacheKey(messages, functions);
|
||||
const cached = await this.redis.get(`claude:cache:${cacheKey}`);
|
||||
|
||||
if (cached) {
|
||||
this.metrics.cacheHits.inc();
|
||||
logger.debug('Cache hit for Claude call');
|
||||
return cached;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Prepare function schemas if any
|
||||
let tools = undefined;
|
||||
if (functions.length > 0) {
|
||||
tools = functions.map(funcName =>
|
||||
this.functionRegistry.getFunctionSchema(funcName)
|
||||
).filter(Boolean);
|
||||
}
|
||||
|
||||
// Make API call
|
||||
const response = await this.client.messages.create({
|
||||
model: process.env.CLAUDE_MODEL || 'claude-3-opus-20240229',
|
||||
max_tokens: maxTokens,
|
||||
temperature: temperature,
|
||||
messages: messages,
|
||||
tools: tools
|
||||
});
|
||||
|
||||
// Track metrics
|
||||
const latency = (Date.now() - startTime) / 1000;
|
||||
this.metrics.apiCalls.inc({ status: 'success', function: functions[0] || 'none' });
|
||||
this.metrics.apiLatency.observe(latency);
|
||||
this.metrics.tokenUsage.inc({ type: 'input' }, response.usage.input_tokens);
|
||||
this.metrics.tokenUsage.inc({ type: 'output' }, response.usage.output_tokens);
|
||||
|
||||
// Process response
|
||||
const result = {
|
||||
content: response.content[0].text,
|
||||
usage: response.usage,
|
||||
function_calls: []
|
||||
};
|
||||
|
||||
// Handle function calls if any
|
||||
if (response.content.some(c => c.type === 'tool_use')) {
|
||||
for (const content of response.content) {
|
||||
if (content.type === 'tool_use') {
|
||||
const functionResult = await this.executeFunctionCall(
|
||||
content.name,
|
||||
content.input
|
||||
);
|
||||
|
||||
result.function_calls.push({
|
||||
name: content.name,
|
||||
input: content.input,
|
||||
result: functionResult
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
await this.redis.setWithExpiry(
|
||||
`claude:cache:${cacheKey}`,
|
||||
result,
|
||||
300 // 5 minutes cache
|
||||
);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.metrics.apiCalls.inc({ status: 'error', function: functions[0] || 'none' });
|
||||
logger.error('Claude API call failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async executeFunctionCall(functionName, parameters) {
|
||||
try {
|
||||
const func = this.functionRegistry.getFunction(functionName);
|
||||
if (!func) {
|
||||
throw new Error(`Function not found: ${functionName}`);
|
||||
}
|
||||
|
||||
const result = await func.execute(parameters);
|
||||
logger.info(`Function executed: ${functionName}`, { result });
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error(`Function execution failed: ${functionName}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
generateCacheKey(messages, functions) {
|
||||
const content = JSON.stringify({ messages, functions });
|
||||
// Simple hash function
|
||||
let hash = 0;
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
const char = content.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // Convert to 32bit integer
|
||||
}
|
||||
return hash.toString(16);
|
||||
}
|
||||
|
||||
async checkHealth() {
|
||||
try {
|
||||
// Simple health check - verify API key is set
|
||||
return !!process.env.CLAUDE_API_KEY;
|
||||
} catch (error) {
|
||||
logger.error('Claude health check failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getMetrics() {
|
||||
return promClient.register.metrics();
|
||||
}
|
||||
|
||||
async getUsageStats(timeRange = '24h') {
|
||||
const stats = await this.redis.hgetall('claude:usage:stats');
|
||||
return {
|
||||
totalCalls: parseInt(stats.totalCalls || 0),
|
||||
totalTokens: parseInt(stats.totalTokens || 0),
|
||||
averageLatency: parseFloat(stats.averageLatency || 0),
|
||||
cacheHitRate: parseFloat(stats.cacheHitRate || 0),
|
||||
timeRange
|
||||
};
|
||||
}
|
||||
|
||||
async updateUsageStats(usage, latency) {
|
||||
await this.redis.hincrby('claude:usage:stats', 'totalCalls', 1);
|
||||
await this.redis.hincrby('claude:usage:stats', 'totalTokens',
|
||||
usage.input_tokens + usage.output_tokens);
|
||||
|
||||
// Update average latency (simplified calculation)
|
||||
const currentStats = await this.redis.hgetall('claude:usage:stats');
|
||||
const totalCalls = parseInt(currentStats.totalCalls || 0);
|
||||
const currentAvg = parseFloat(currentStats.averageLatency || 0);
|
||||
const newAvg = (currentAvg * (totalCalls - 1) + latency) / totalCalls;
|
||||
|
||||
await this.redis.hset('claude:usage:stats', 'averageLatency', newAvg.toFixed(3));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { RedisClient } from '../config/redis.js';
|
||||
|
||||
export class ContextManager {
|
||||
constructor() {
|
||||
this.redis = null;
|
||||
this.maxContextSize = 100000; // Maximum context size in characters
|
||||
this.contextTTL = 3600; // 1 hour TTL for context
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (!ContextManager.instance) {
|
||||
ContextManager.instance = new ContextManager();
|
||||
ContextManager.instance.initialize();
|
||||
}
|
||||
return ContextManager.instance;
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.redis = RedisClient.getInstance();
|
||||
logger.info('Context manager initialized');
|
||||
}
|
||||
|
||||
async createContext(sessionId, initialData = {}) {
|
||||
const context = {
|
||||
sessionId,
|
||||
createdAt: new Date().toISOString(),
|
||||
messages: [],
|
||||
metadata: initialData,
|
||||
tokenCount: 0
|
||||
};
|
||||
|
||||
await this.saveContext(sessionId, context);
|
||||
return context;
|
||||
}
|
||||
|
||||
async getContext(sessionId) {
|
||||
const key = `context:${sessionId}`;
|
||||
const context = await this.redis.get(key);
|
||||
|
||||
if (!context) {
|
||||
return await this.createContext(sessionId);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
async saveContext(sessionId, context) {
|
||||
const key = `context:${sessionId}`;
|
||||
await this.redis.setWithExpiry(key, context, this.contextTTL);
|
||||
}
|
||||
|
||||
async addMessage(sessionId, role, content, metadata = {}) {
|
||||
const context = await this.getContext(sessionId);
|
||||
|
||||
const message = {
|
||||
role,
|
||||
content,
|
||||
timestamp: new Date().toISOString(),
|
||||
metadata
|
||||
};
|
||||
|
||||
context.messages.push(message);
|
||||
|
||||
// Update token count (approximate)
|
||||
context.tokenCount += this.estimateTokens(content);
|
||||
|
||||
// Trim context if too large
|
||||
if (context.tokenCount > this.maxContextSize) {
|
||||
context.messages = this.trimMessages(context.messages);
|
||||
context.tokenCount = this.calculateTotalTokens(context.messages);
|
||||
}
|
||||
|
||||
await this.saveContext(sessionId, context);
|
||||
return context;
|
||||
}
|
||||
|
||||
async updateMetadata(sessionId, metadata) {
|
||||
const context = await this.getContext(sessionId);
|
||||
context.metadata = {
|
||||
...context.metadata,
|
||||
...metadata
|
||||
};
|
||||
|
||||
await this.saveContext(sessionId, context);
|
||||
return context;
|
||||
}
|
||||
|
||||
async getConversationHistory(sessionId, limit = 10) {
|
||||
const context = await this.getContext(sessionId);
|
||||
const messages = context.messages.slice(-limit);
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
async summarizeContext(sessionId) {
|
||||
const context = await this.getContext(sessionId);
|
||||
|
||||
if (context.messages.length < 10) {
|
||||
return null; // No need to summarize short conversations
|
||||
}
|
||||
|
||||
// Group messages by topic/time periods
|
||||
const summary = {
|
||||
sessionId,
|
||||
messageCount: context.messages.length,
|
||||
duration: this.calculateDuration(context),
|
||||
topics: this.extractTopics(context.messages),
|
||||
keyDecisions: this.extractKeyDecisions(context.messages),
|
||||
actionItems: this.extractActionItems(context.messages)
|
||||
};
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
async archiveContext(sessionId) {
|
||||
const context = await this.getContext(sessionId);
|
||||
|
||||
// Save to long-term storage (MongoDB in production)
|
||||
const archiveKey = `context:archive:${sessionId}`;
|
||||
await this.redis.setWithExpiry(archiveKey, context, 86400 * 7); // 7 days
|
||||
|
||||
// Clear active context
|
||||
await this.clearContext(sessionId);
|
||||
|
||||
logger.info(`Context archived: ${sessionId}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
async clearContext(sessionId) {
|
||||
const key = `context:${sessionId}`;
|
||||
await this.redis.del(key);
|
||||
logger.info(`Context cleared: ${sessionId}`);
|
||||
}
|
||||
|
||||
async mergeContexts(primarySessionId, secondarySessionId) {
|
||||
const primaryContext = await this.getContext(primarySessionId);
|
||||
const secondaryContext = await this.getContext(secondarySessionId);
|
||||
|
||||
// Merge messages chronologically
|
||||
const allMessages = [...primaryContext.messages, ...secondaryContext.messages]
|
||||
.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
||||
|
||||
primaryContext.messages = allMessages;
|
||||
primaryContext.metadata = {
|
||||
...secondaryContext.metadata,
|
||||
...primaryContext.metadata,
|
||||
mergedFrom: secondarySessionId,
|
||||
mergedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
await this.saveContext(primarySessionId, primaryContext);
|
||||
await this.clearContext(secondarySessionId);
|
||||
|
||||
return primaryContext;
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
estimateTokens(text) {
|
||||
// Rough estimation: 1 token ≈ 4 characters
|
||||
return Math.ceil(text.length / 4);
|
||||
}
|
||||
|
||||
calculateTotalTokens(messages) {
|
||||
return messages.reduce((total, msg) =>
|
||||
total + this.estimateTokens(msg.content), 0
|
||||
);
|
||||
}
|
||||
|
||||
trimMessages(messages) {
|
||||
// Keep system messages and recent messages
|
||||
const systemMessages = messages.filter(m => m.role === 'system');
|
||||
const recentMessages = messages.slice(-20);
|
||||
|
||||
// Combine, removing duplicates
|
||||
const trimmed = [...systemMessages];
|
||||
recentMessages.forEach(msg => {
|
||||
if (!trimmed.find(m => m.timestamp === msg.timestamp)) {
|
||||
trimmed.push(msg);
|
||||
}
|
||||
});
|
||||
|
||||
return trimmed.sort((a, b) =>
|
||||
new Date(a.timestamp) - new Date(b.timestamp)
|
||||
);
|
||||
}
|
||||
|
||||
calculateDuration(context) {
|
||||
if (context.messages.length === 0) return 0;
|
||||
|
||||
const firstMessage = context.messages[0];
|
||||
const lastMessage = context.messages[context.messages.length - 1];
|
||||
|
||||
const duration = new Date(lastMessage.timestamp) - new Date(firstMessage.timestamp);
|
||||
return Math.round(duration / 1000 / 60); // Duration in minutes
|
||||
}
|
||||
|
||||
extractTopics(messages) {
|
||||
// Simple topic extraction based on keywords
|
||||
const topics = new Map();
|
||||
const keywords = [
|
||||
'campaign', 'audience', 'content', 'strategy',
|
||||
'budget', 'performance', 'engagement', 'conversion'
|
||||
];
|
||||
|
||||
messages.forEach(msg => {
|
||||
const content = msg.content.toLowerCase();
|
||||
keywords.forEach(keyword => {
|
||||
if (content.includes(keyword)) {
|
||||
topics.set(keyword, (topics.get(keyword) || 0) + 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Return top 5 topics
|
||||
return Array.from(topics.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 5)
|
||||
.map(([topic]) => topic);
|
||||
}
|
||||
|
||||
extractKeyDecisions(messages) {
|
||||
// Extract messages that contain decision-making language
|
||||
const decisionKeywords = [
|
||||
'decided', 'approve', 'confirm', 'agree', 'select',
|
||||
'choose', 'proceed', 'implement', 'launch'
|
||||
];
|
||||
|
||||
return messages
|
||||
.filter(msg => {
|
||||
const content = msg.content.toLowerCase();
|
||||
return decisionKeywords.some(keyword => content.includes(keyword));
|
||||
})
|
||||
.slice(-5) // Last 5 decisions
|
||||
.map(msg => ({
|
||||
timestamp: msg.timestamp,
|
||||
summary: msg.content.substring(0, 100) + '...'
|
||||
}));
|
||||
}
|
||||
|
||||
extractActionItems(messages) {
|
||||
// Extract messages that contain action-oriented language
|
||||
const actionKeywords = [
|
||||
'will', 'todo', 'task', 'action', 'next step',
|
||||
'follow up', 'implement', 'create', 'build'
|
||||
];
|
||||
|
||||
return messages
|
||||
.filter(msg => {
|
||||
const content = msg.content.toLowerCase();
|
||||
return actionKeywords.some(keyword => content.includes(keyword));
|
||||
})
|
||||
.slice(-10) // Last 10 action items
|
||||
.map(msg => ({
|
||||
timestamp: msg.timestamp,
|
||||
summary: msg.content.substring(0, 100) + '...'
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
import { z } from 'zod';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import axios from 'axios';
|
||||
|
||||
export class FunctionRegistry {
|
||||
constructor() {
|
||||
this.functions = new Map();
|
||||
this.schemas = new Map();
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (!FunctionRegistry.instance) {
|
||||
FunctionRegistry.instance = new FunctionRegistry();
|
||||
}
|
||||
return FunctionRegistry.instance;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
// Register all available functions
|
||||
this.registerAnalysisFunctions();
|
||||
this.registerContentFunctions();
|
||||
this.registerCampaignFunctions();
|
||||
this.registerDataFunctions();
|
||||
|
||||
logger.info(`Function registry initialized with ${this.functions.size} functions`);
|
||||
}
|
||||
|
||||
registerFunction(name, schema, handler) {
|
||||
this.functions.set(name, {
|
||||
name,
|
||||
handler,
|
||||
schema
|
||||
});
|
||||
|
||||
this.schemas.set(name, {
|
||||
name,
|
||||
description: schema.description,
|
||||
input_schema: this.zodToJsonSchema(schema.parameters)
|
||||
});
|
||||
|
||||
logger.debug(`Function registered: ${name}`);
|
||||
}
|
||||
|
||||
registerAnalysisFunctions() {
|
||||
// Analyze audience function
|
||||
this.registerFunction('analyze_audience', {
|
||||
description: 'Analyze target audience characteristics and behaviors',
|
||||
parameters: z.object({
|
||||
audienceData: z.object({
|
||||
demographics: z.array(z.string()).optional(),
|
||||
interests: z.array(z.string()).optional(),
|
||||
behaviors: z.array(z.string()).optional(),
|
||||
locations: z.array(z.string()).optional()
|
||||
})
|
||||
})
|
||||
}, async (params) => {
|
||||
// Simulate audience analysis
|
||||
return {
|
||||
segments: [
|
||||
{
|
||||
name: 'Primary',
|
||||
size: 10000,
|
||||
characteristics: ['tech-savvy', 'early-adopters'],
|
||||
engagement_rate: 0.15
|
||||
},
|
||||
{
|
||||
name: 'Secondary',
|
||||
size: 25000,
|
||||
characteristics: ['mainstream', 'price-conscious'],
|
||||
engagement_rate: 0.08
|
||||
}
|
||||
],
|
||||
insights: [
|
||||
'High concentration in urban areas',
|
||||
'Peak activity between 7-9 PM',
|
||||
'Prefer visual content over text'
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
// Check content safety
|
||||
this.registerFunction('check_content_safety', {
|
||||
description: 'Check if content violates safety guidelines',
|
||||
parameters: z.object({
|
||||
content: z.string(),
|
||||
platform: z.enum(['telegram', 'whatsapp', 'facebook']).optional()
|
||||
})
|
||||
}, async (params) => {
|
||||
// Call safety service
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${process.env.SAFETY_GUARD_URL}/api/v1/check`,
|
||||
params
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
logger.error('Safety check failed:', error);
|
||||
return {
|
||||
safe: false,
|
||||
violations: ['service_unavailable'],
|
||||
confidence: 0
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Analyze sentiment
|
||||
this.registerFunction('analyze_sentiment', {
|
||||
description: 'Analyze sentiment of text content',
|
||||
parameters: z.object({
|
||||
text: z.string(),
|
||||
language: z.string().optional()
|
||||
})
|
||||
}, async (params) => {
|
||||
// Simple sentiment analysis (in production, use a proper NLP service)
|
||||
const positiveWords = ['good', 'great', 'excellent', 'love', 'amazing'];
|
||||
const negativeWords = ['bad', 'terrible', 'hate', 'awful', 'poor'];
|
||||
|
||||
const text = params.text.toLowerCase();
|
||||
let score = 0;
|
||||
|
||||
positiveWords.forEach(word => {
|
||||
if (text.includes(word)) score += 1;
|
||||
});
|
||||
|
||||
negativeWords.forEach(word => {
|
||||
if (text.includes(word)) score -= 1;
|
||||
});
|
||||
|
||||
return {
|
||||
sentiment: score > 0 ? 'positive' : score < 0 ? 'negative' : 'neutral',
|
||||
score: Math.max(-1, Math.min(1, score / 5)),
|
||||
confidence: 0.7
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
registerContentFunctions() {
|
||||
// Generate content ideas
|
||||
this.registerFunction('generate_content_ideas', {
|
||||
description: 'Generate content ideas for marketing campaign',
|
||||
parameters: z.object({
|
||||
topic: z.string(),
|
||||
audience: z.string(),
|
||||
count: z.number().min(1).max(10).default(5),
|
||||
contentType: z.enum(['text', 'image', 'video', 'mixed']).optional()
|
||||
})
|
||||
}, async (params) => {
|
||||
// Generate content ideas based on parameters
|
||||
const ideas = [];
|
||||
for (let i = 0; i < params.count; i++) {
|
||||
ideas.push({
|
||||
title: `${params.topic} Content Idea ${i + 1}`,
|
||||
description: `Engaging content about ${params.topic} for ${params.audience}`,
|
||||
type: params.contentType || 'text',
|
||||
estimatedEngagement: Math.random() * 0.2 + 0.05
|
||||
});
|
||||
}
|
||||
return { ideas };
|
||||
});
|
||||
|
||||
// Extract entities
|
||||
this.registerFunction('extract_entities', {
|
||||
description: 'Extract named entities from text',
|
||||
parameters: z.object({
|
||||
text: z.string(),
|
||||
entityTypes: z.array(z.enum(['person', 'organization', 'location', 'date', 'url', 'email']))
|
||||
.optional()
|
||||
})
|
||||
}, async (params) => {
|
||||
// Simple entity extraction (in production, use NLP service)
|
||||
const entities = [];
|
||||
|
||||
// Extract URLs
|
||||
const urlRegex = /https?:\/\/[^\s]+/g;
|
||||
const urls = params.text.match(urlRegex) || [];
|
||||
urls.forEach(url => {
|
||||
entities.push({ type: 'url', value: url });
|
||||
});
|
||||
|
||||
// Extract emails
|
||||
const emailRegex = /[^\s]+@[^\s]+\.[^\s]+/g;
|
||||
const emails = params.text.match(emailRegex) || [];
|
||||
emails.forEach(email => {
|
||||
entities.push({ type: 'email', value: email });
|
||||
});
|
||||
|
||||
return { entities };
|
||||
});
|
||||
}
|
||||
|
||||
registerCampaignFunctions() {
|
||||
// Plan schedule
|
||||
this.registerFunction('plan_schedule', {
|
||||
description: 'Plan optimal posting schedule for campaign',
|
||||
parameters: z.object({
|
||||
duration: z.object({
|
||||
start: z.string(),
|
||||
end: z.string()
|
||||
}),
|
||||
frequency: z.enum(['hourly', 'daily', 'weekly']),
|
||||
timezone: z.string().default('UTC')
|
||||
})
|
||||
}, async (params) => {
|
||||
// Generate schedule based on best practices
|
||||
const schedule = [];
|
||||
const startDate = new Date(params.duration.start);
|
||||
const endDate = new Date(params.duration.end);
|
||||
|
||||
let currentDate = new Date(startDate);
|
||||
while (currentDate <= endDate) {
|
||||
// Best posting times (simplified)
|
||||
const postTime = new Date(currentDate);
|
||||
postTime.setHours(14, 0, 0, 0); // 2 PM
|
||||
|
||||
schedule.push({
|
||||
datetime: postTime.toISOString(),
|
||||
type: 'post',
|
||||
priority: 'normal'
|
||||
});
|
||||
|
||||
// Increment based on frequency
|
||||
switch (params.frequency) {
|
||||
case 'hourly':
|
||||
currentDate.setHours(currentDate.getHours() + 1);
|
||||
break;
|
||||
case 'daily':
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
break;
|
||||
case 'weekly':
|
||||
currentDate.setDate(currentDate.getDate() + 7);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { schedule };
|
||||
});
|
||||
|
||||
// Estimate results
|
||||
this.registerFunction('estimate_results', {
|
||||
description: 'Estimate campaign results based on parameters',
|
||||
parameters: z.object({
|
||||
audienceSize: z.number(),
|
||||
budget: z.number(),
|
||||
duration: z.number(), // days
|
||||
contentQuality: z.enum(['low', 'medium', 'high']).default('medium')
|
||||
})
|
||||
}, async (params) => {
|
||||
// Simple estimation model
|
||||
const baseEngagementRate = {
|
||||
low: 0.02,
|
||||
medium: 0.05,
|
||||
high: 0.10
|
||||
};
|
||||
|
||||
const engagementRate = baseEngagementRate[params.contentQuality];
|
||||
const reach = Math.min(params.audienceSize, params.budget * 100);
|
||||
const impressions = reach * (params.duration / 7); // Weekly multiplier
|
||||
const engagements = impressions * engagementRate;
|
||||
const conversions = engagements * 0.1; // 10% conversion rate
|
||||
|
||||
return {
|
||||
estimatedReach: Math.round(reach),
|
||||
estimatedImpressions: Math.round(impressions),
|
||||
estimatedEngagements: Math.round(engagements),
|
||||
estimatedConversions: Math.round(conversions),
|
||||
estimatedROI: (conversions * 50 - params.budget) / params.budget,
|
||||
confidence: 0.6
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
registerDataFunctions() {
|
||||
// Fetch analytics data
|
||||
this.registerFunction('fetch_analytics', {
|
||||
description: 'Fetch analytics data for a specific period',
|
||||
parameters: z.object({
|
||||
metric: z.enum(['impressions', 'clicks', 'conversions', 'engagement']),
|
||||
period: z.object({
|
||||
start: z.string(),
|
||||
end: z.string()
|
||||
}),
|
||||
groupBy: z.enum(['hour', 'day', 'week']).optional()
|
||||
})
|
||||
}, async (params) => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${process.env.ANALYTICS_URL}/api/v1/metrics`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch analytics:', error);
|
||||
return { error: 'Failed to fetch analytics data' };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getFunction(name) {
|
||||
return this.functions.get(name);
|
||||
}
|
||||
|
||||
getFunctionSchema(name) {
|
||||
return this.schemas.get(name);
|
||||
}
|
||||
|
||||
getAllFunctions() {
|
||||
return Array.from(this.functions.keys());
|
||||
}
|
||||
|
||||
zodToJsonSchema(zodSchema) {
|
||||
// Convert Zod schema to JSON Schema format expected by Claude
|
||||
// This is a simplified version - in production, use a proper converter
|
||||
if (zodSchema instanceof z.ZodObject) {
|
||||
const shape = zodSchema.shape;
|
||||
const properties = {};
|
||||
const required = [];
|
||||
|
||||
for (const [key, value] of Object.entries(shape)) {
|
||||
properties[key] = this.zodFieldToJsonSchema(value);
|
||||
if (!value.isOptional()) {
|
||||
required.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'object',
|
||||
properties,
|
||||
required: required.length > 0 ? required : undefined
|
||||
};
|
||||
}
|
||||
|
||||
return this.zodFieldToJsonSchema(zodSchema);
|
||||
}
|
||||
|
||||
zodFieldToJsonSchema(field) {
|
||||
if (field instanceof z.ZodString) {
|
||||
return { type: 'string' };
|
||||
} else if (field instanceof z.ZodNumber) {
|
||||
return { type: 'number' };
|
||||
} else if (field instanceof z.ZodBoolean) {
|
||||
return { type: 'boolean' };
|
||||
} else if (field instanceof z.ZodArray) {
|
||||
return {
|
||||
type: 'array',
|
||||
items: this.zodFieldToJsonSchema(field.element)
|
||||
};
|
||||
} else if (field instanceof z.ZodObject) {
|
||||
return this.zodToJsonSchema(field);
|
||||
} else if (field instanceof z.ZodEnum) {
|
||||
return {
|
||||
type: 'string',
|
||||
enum: field.options
|
||||
};
|
||||
} else if (field instanceof z.ZodOptional) {
|
||||
return this.zodFieldToJsonSchema(field.unwrap());
|
||||
}
|
||||
|
||||
return { type: 'string' }; // Default fallback
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,661 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { cache } from '../utils/cache.js';
|
||||
|
||||
/**
|
||||
* Marketing Intelligence Service powered by Claude AI
|
||||
*/
|
||||
export class MarketingIntelligence {
|
||||
constructor() {
|
||||
this.anthropic = new Anthropic({
|
||||
apiKey: process.env.ANTHROPIC_API_KEY || 'your-api-key'
|
||||
});
|
||||
|
||||
this.systemPrompt = `You are an expert marketing strategist specializing in Telegram marketing campaigns.
|
||||
Your role is to provide intelligent, data-driven marketing suggestions that are ethical, effective, and compliant with regulations.
|
||||
|
||||
Key responsibilities:
|
||||
1. Analyze campaign performance and suggest improvements
|
||||
2. Generate creative and engaging message content
|
||||
3. Identify optimal timing and audience segments
|
||||
4. Provide A/B testing recommendations
|
||||
5. Ensure compliance with anti-spam regulations
|
||||
6. Suggest personalization strategies
|
||||
7. Recommend engagement optimization tactics
|
||||
|
||||
Always prioritize user value, ethical practices, and long-term relationship building over short-term gains.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze campaign and provide suggestions
|
||||
*/
|
||||
async analyzeCampaign(campaignData) {
|
||||
try {
|
||||
const prompt = this.buildCampaignAnalysisPrompt(campaignData);
|
||||
|
||||
const response = await this.anthropic.messages.create({
|
||||
model: 'claude-3-opus-20240229',
|
||||
max_tokens: 2000,
|
||||
temperature: 0.7,
|
||||
system: this.systemPrompt,
|
||||
messages: [{ role: 'user', content: prompt }]
|
||||
});
|
||||
|
||||
const analysis = this.parseAnalysisResponse(response.content[0].text);
|
||||
|
||||
// Cache the analysis
|
||||
await this.cacheAnalysis(campaignData.id, analysis);
|
||||
|
||||
return analysis;
|
||||
} catch (error) {
|
||||
logger.error('Campaign analysis failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate optimized message content
|
||||
*/
|
||||
async generateMessageContent(context) {
|
||||
try {
|
||||
const prompt = this.buildMessageGenerationPrompt(context);
|
||||
|
||||
const response = await this.anthropic.messages.create({
|
||||
model: 'claude-3-opus-20240229',
|
||||
max_tokens: 1000,
|
||||
temperature: 0.8,
|
||||
system: this.systemPrompt,
|
||||
messages: [{ role: 'user', content: prompt }]
|
||||
});
|
||||
|
||||
const suggestions = this.parseMessageSuggestions(response.content[0].text);
|
||||
|
||||
return suggestions;
|
||||
} catch (error) {
|
||||
logger.error('Message generation failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest audience segments
|
||||
*/
|
||||
async suggestAudienceSegments(userData) {
|
||||
try {
|
||||
const prompt = this.buildSegmentationPrompt(userData);
|
||||
|
||||
const response = await this.anthropic.messages.create({
|
||||
model: 'claude-3-opus-20240229',
|
||||
max_tokens: 1500,
|
||||
temperature: 0.6,
|
||||
system: this.systemPrompt,
|
||||
messages: [{ role: 'user', content: prompt }]
|
||||
});
|
||||
|
||||
const segments = this.parseSegmentationResponse(response.content[0].text);
|
||||
|
||||
return segments;
|
||||
} catch (error) {
|
||||
logger.error('Audience segmentation failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize campaign timing
|
||||
*/
|
||||
async optimizeTiming(historicalData) {
|
||||
try {
|
||||
const prompt = this.buildTimingOptimizationPrompt(historicalData);
|
||||
|
||||
const response = await this.anthropic.messages.create({
|
||||
model: 'claude-3-opus-20240229',
|
||||
max_tokens: 1000,
|
||||
temperature: 0.5,
|
||||
system: this.systemPrompt,
|
||||
messages: [{ role: 'user', content: prompt }]
|
||||
});
|
||||
|
||||
const timingRecommendations = this.parseTimingResponse(response.content[0].text);
|
||||
|
||||
return timingRecommendations;
|
||||
} catch (error) {
|
||||
logger.error('Timing optimization failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate A/B test recommendations
|
||||
*/
|
||||
async generateABTestRecommendations(campaignContext) {
|
||||
try {
|
||||
const prompt = this.buildABTestPrompt(campaignContext);
|
||||
|
||||
const response = await this.anthropic.messages.create({
|
||||
model: 'claude-3-opus-20240229',
|
||||
max_tokens: 1500,
|
||||
temperature: 0.7,
|
||||
system: this.systemPrompt,
|
||||
messages: [{ role: 'user', content: prompt }]
|
||||
});
|
||||
|
||||
const recommendations = this.parseABTestResponse(response.content[0].text);
|
||||
|
||||
return recommendations;
|
||||
} catch (error) {
|
||||
logger.error('A/B test generation failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide compliance check
|
||||
*/
|
||||
async checkCompliance(messageContent, targetRegions) {
|
||||
try {
|
||||
const prompt = this.buildCompliancePrompt(messageContent, targetRegions);
|
||||
|
||||
const response = await this.anthropic.messages.create({
|
||||
model: 'claude-3-opus-20240229',
|
||||
max_tokens: 1000,
|
||||
temperature: 0.3,
|
||||
system: this.systemPrompt,
|
||||
messages: [{ role: 'user', content: prompt }]
|
||||
});
|
||||
|
||||
const complianceCheck = this.parseComplianceResponse(response.content[0].text);
|
||||
|
||||
return complianceCheck;
|
||||
} catch (error) {
|
||||
logger.error('Compliance check failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest engagement strategies
|
||||
*/
|
||||
async suggestEngagementStrategies(campaignMetrics) {
|
||||
try {
|
||||
const prompt = this.buildEngagementPrompt(campaignMetrics);
|
||||
|
||||
const response = await this.anthropic.messages.create({
|
||||
model: 'claude-3-opus-20240229',
|
||||
max_tokens: 1500,
|
||||
temperature: 0.7,
|
||||
system: this.systemPrompt,
|
||||
messages: [{ role: 'user', content: prompt }]
|
||||
});
|
||||
|
||||
const strategies = this.parseEngagementResponse(response.content[0].text);
|
||||
|
||||
return strategies;
|
||||
} catch (error) {
|
||||
logger.error('Engagement strategy generation failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build prompts for different analyses
|
||||
*/
|
||||
buildCampaignAnalysisPrompt(campaignData) {
|
||||
return `Analyze the following Telegram marketing campaign and provide detailed suggestions for improvement:
|
||||
|
||||
Campaign Name: ${campaignData.name}
|
||||
Target Audience: ${JSON.stringify(campaignData.targetAudience)}
|
||||
Message Content: ${campaignData.messageContent}
|
||||
Current Metrics:
|
||||
- Messages Sent: ${campaignData.metrics.sent}
|
||||
- Open Rate: ${campaignData.metrics.openRate}%
|
||||
- Click Rate: ${campaignData.metrics.clickRate}%
|
||||
- Conversion Rate: ${campaignData.metrics.conversionRate}%
|
||||
- Unsubscribe Rate: ${campaignData.metrics.unsubscribeRate}%
|
||||
|
||||
Historical Performance: ${JSON.stringify(campaignData.historicalMetrics)}
|
||||
|
||||
Please provide:
|
||||
1. Overall campaign assessment
|
||||
2. Specific improvements for message content
|
||||
3. Audience targeting suggestions
|
||||
4. Timing optimization recommendations
|
||||
5. Engagement improvement tactics
|
||||
6. Risk factors and mitigation strategies`;
|
||||
}
|
||||
|
||||
buildMessageGenerationPrompt(context) {
|
||||
return `Generate 3 variations of marketing messages for the following context:
|
||||
|
||||
Product/Service: ${context.product}
|
||||
Target Audience: ${context.targetAudience}
|
||||
Campaign Goal: ${context.goal}
|
||||
Tone: ${context.tone}
|
||||
Key Features: ${context.features.join(', ')}
|
||||
Call to Action: ${context.cta}
|
||||
Character Limit: ${context.characterLimit || 'None'}
|
||||
|
||||
Additional Context: ${context.additionalInfo || 'None'}
|
||||
|
||||
For each variation, provide:
|
||||
1. The message text
|
||||
2. Key psychological triggers used
|
||||
3. Expected engagement level (1-10)
|
||||
4. Best time to send
|
||||
5. Personalization opportunities`;
|
||||
}
|
||||
|
||||
buildSegmentationPrompt(userData) {
|
||||
return `Based on the following user data, suggest optimal audience segments for targeted marketing:
|
||||
|
||||
Total Users: ${userData.totalUsers}
|
||||
Demographics:
|
||||
${JSON.stringify(userData.demographics, null, 2)}
|
||||
|
||||
Behavioral Data:
|
||||
${JSON.stringify(userData.behavioral, null, 2)}
|
||||
|
||||
Engagement History:
|
||||
${JSON.stringify(userData.engagementHistory, null, 2)}
|
||||
|
||||
Please provide:
|
||||
1. 5-7 distinct audience segments
|
||||
2. Key characteristics of each segment
|
||||
3. Recommended messaging approach for each
|
||||
4. Expected response rates
|
||||
5. Segment size estimates`;
|
||||
}
|
||||
|
||||
buildTimingOptimizationPrompt(historicalData) {
|
||||
return `Analyze the following historical engagement data and recommend optimal timing for message delivery:
|
||||
|
||||
Historical Send Times and Results:
|
||||
${JSON.stringify(historicalData.sendTimes, null, 2)}
|
||||
|
||||
User Activity Patterns:
|
||||
${JSON.stringify(historicalData.activityPatterns, null, 2)}
|
||||
|
||||
Time Zone Distribution:
|
||||
${JSON.stringify(historicalData.timeZones, null, 2)}
|
||||
|
||||
Please provide:
|
||||
1. Top 3 optimal time windows for sending
|
||||
2. Day of week recommendations
|
||||
3. Time zone-specific strategies
|
||||
4. Avoid times (low engagement periods)
|
||||
5. Special timing for different message types`;
|
||||
}
|
||||
|
||||
buildABTestPrompt(campaignContext) {
|
||||
return `Design A/B test recommendations for the following campaign:
|
||||
|
||||
Campaign Type: ${campaignContext.type}
|
||||
Current Message: ${campaignContext.currentMessage}
|
||||
Target Metrics: ${campaignContext.targetMetrics.join(', ')}
|
||||
Audience Size: ${campaignContext.audienceSize}
|
||||
Test Duration Available: ${campaignContext.duration}
|
||||
|
||||
Please provide:
|
||||
1. 3-5 test variations with hypotheses
|
||||
2. Variables to test (one per test)
|
||||
3. Sample size recommendations
|
||||
4. Success metrics and thresholds
|
||||
5. Statistical significance requirements
|
||||
6. Test sequence if multiple tests needed`;
|
||||
}
|
||||
|
||||
buildCompliancePrompt(messageContent, targetRegions) {
|
||||
return `Review the following message for compliance with marketing regulations:
|
||||
|
||||
Message Content:
|
||||
"${messageContent}"
|
||||
|
||||
Target Regions: ${targetRegions.join(', ')}
|
||||
|
||||
Please check for:
|
||||
1. GDPR compliance (if EU targeted)
|
||||
2. CAN-SPAM compliance
|
||||
3. TCPA compliance (if US targeted)
|
||||
4. Unsubscribe mechanism requirements
|
||||
5. Consent verification needs
|
||||
6. Data protection considerations
|
||||
7. Age restriction requirements
|
||||
8. Prohibited content or claims
|
||||
|
||||
Provide a compliance score (0-100) and specific recommendations for each region.`;
|
||||
}
|
||||
|
||||
buildEngagementPrompt(campaignMetrics) {
|
||||
return `Based on the following campaign metrics, suggest strategies to improve engagement:
|
||||
|
||||
Current Metrics:
|
||||
${JSON.stringify(campaignMetrics.current, null, 2)}
|
||||
|
||||
Historical Trends:
|
||||
${JSON.stringify(campaignMetrics.trends, null, 2)}
|
||||
|
||||
Audience Feedback:
|
||||
${JSON.stringify(campaignMetrics.feedback, null, 2)}
|
||||
|
||||
Please provide:
|
||||
1. Quick wins (implementable immediately)
|
||||
2. Medium-term strategies (1-4 weeks)
|
||||
3. Long-term engagement building (1-3 months)
|
||||
4. Interactive element suggestions
|
||||
5. Community building tactics
|
||||
6. Retention improvement methods`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse response methods
|
||||
*/
|
||||
parseAnalysisResponse(response) {
|
||||
// Parse the structured response from Claude
|
||||
// This is a simplified version - in production, use more robust parsing
|
||||
const sections = response.split('\n\n');
|
||||
|
||||
return {
|
||||
overallAssessment: sections[0] || '',
|
||||
messageImprovements: sections[1] || '',
|
||||
audienceSuggestions: sections[2] || '',
|
||||
timingRecommendations: sections[3] || '',
|
||||
engagementTactics: sections[4] || '',
|
||||
riskFactors: sections[5] || '',
|
||||
summary: {
|
||||
score: this.extractScore(response),
|
||||
priority: this.extractPriority(response),
|
||||
estimatedImprovement: this.extractImprovement(response)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
parseMessageSuggestions(response) {
|
||||
const variations = [];
|
||||
const parts = response.split(/Variation \d+:/i);
|
||||
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const variation = this.parseMessageVariation(parts[i]);
|
||||
if (variation) {
|
||||
variations.push(variation);
|
||||
}
|
||||
}
|
||||
|
||||
return variations;
|
||||
}
|
||||
|
||||
parseMessageVariation(text) {
|
||||
// Extract message components
|
||||
const messageMatch = text.match(/Message Text:(.*?)Key Psychological/is);
|
||||
const triggersMatch = text.match(/Key Psychological Triggers:(.*?)Expected Engagement/is);
|
||||
const engagementMatch = text.match(/Expected Engagement Level: (\d+)/i);
|
||||
const timeMatch = text.match(/Best Time to Send:(.*?)Personalization/is);
|
||||
const personalizationMatch = text.match(/Personalization Opportunities:(.*?)$/is);
|
||||
|
||||
return {
|
||||
message: messageMatch ? messageMatch[1].trim() : '',
|
||||
psychologicalTriggers: triggersMatch ? triggersMatch[1].trim().split(',').map(t => t.trim()) : [],
|
||||
engagementScore: engagementMatch ? parseInt(engagementMatch[1]) : 5,
|
||||
bestTime: timeMatch ? timeMatch[1].trim() : '',
|
||||
personalizationOptions: personalizationMatch ? personalizationMatch[1].trim().split(',').map(t => t.trim()) : []
|
||||
};
|
||||
}
|
||||
|
||||
parseSegmentationResponse(response) {
|
||||
const segments = [];
|
||||
const parts = response.split(/Segment \d+:/i);
|
||||
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const segment = this.parseSegment(parts[i]);
|
||||
if (segment) {
|
||||
segments.push(segment);
|
||||
}
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
parseSegment(text) {
|
||||
// Extract segment details
|
||||
const nameMatch = text.match(/Name:(.*?)\n/i);
|
||||
const characteristicsMatch = text.match(/Characteristics:(.*?)Messaging Approach:/is);
|
||||
const messagingMatch = text.match(/Messaging Approach:(.*?)Expected Response Rate:/is);
|
||||
const responseMatch = text.match(/Expected Response Rate: ([\d.]+)%/i);
|
||||
const sizeMatch = text.match(/Segment Size: ([\d,]+)/i);
|
||||
|
||||
return {
|
||||
name: nameMatch ? nameMatch[1].trim() : '',
|
||||
characteristics: characteristicsMatch ? characteristicsMatch[1].trim().split('\n').filter(c => c.trim()) : [],
|
||||
messagingApproach: messagingMatch ? messagingMatch[1].trim() : '',
|
||||
expectedResponseRate: responseMatch ? parseFloat(responseMatch[1]) : 0,
|
||||
estimatedSize: sizeMatch ? parseInt(sizeMatch[1].replace(/,/g, '')) : 0
|
||||
};
|
||||
}
|
||||
|
||||
parseTimingResponse(response) {
|
||||
return {
|
||||
optimalWindows: this.extractTimeWindows(response),
|
||||
dayRecommendations: this.extractDayRecommendations(response),
|
||||
timeZoneStrategies: this.extractTimeZoneStrategies(response),
|
||||
avoidPeriods: this.extractAvoidPeriods(response),
|
||||
messageTypeTimings: this.extractMessageTypeTimings(response)
|
||||
};
|
||||
}
|
||||
|
||||
parseABTestResponse(response) {
|
||||
const tests = [];
|
||||
const parts = response.split(/Test \d+:/i);
|
||||
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const test = this.parseABTest(parts[i]);
|
||||
if (test) {
|
||||
tests.push(test);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tests,
|
||||
sequenceRecommendation: this.extractTestSequence(response),
|
||||
overallStrategy: this.extractTestStrategy(response)
|
||||
};
|
||||
}
|
||||
|
||||
parseComplianceResponse(response) {
|
||||
const scoreMatch = response.match(/Compliance Score: (\d+)/i);
|
||||
|
||||
return {
|
||||
score: scoreMatch ? parseInt(scoreMatch[1]) : 0,
|
||||
gdprCompliant: response.includes('GDPR: Compliant') || response.includes('GDPR compliant'),
|
||||
canSpamCompliant: response.includes('CAN-SPAM: Compliant') || response.includes('CAN-SPAM compliant'),
|
||||
regionalIssues: this.extractRegionalIssues(response),
|
||||
recommendations: this.extractComplianceRecommendations(response),
|
||||
requiredChanges: this.extractRequiredChanges(response)
|
||||
};
|
||||
}
|
||||
|
||||
parseEngagementResponse(response) {
|
||||
return {
|
||||
quickWins: this.extractQuickWins(response),
|
||||
mediumTermStrategies: this.extractMediumTermStrategies(response),
|
||||
longTermPlan: this.extractLongTermPlan(response),
|
||||
interactiveElements: this.extractInteractiveElements(response),
|
||||
communityBuilding: this.extractCommunityBuilding(response),
|
||||
retentionMethods: this.extractRetentionMethods(response)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper extraction methods
|
||||
*/
|
||||
extractScore(text) {
|
||||
const match = text.match(/Score: (\d+)/i);
|
||||
return match ? parseInt(match[1]) : null;
|
||||
}
|
||||
|
||||
extractPriority(text) {
|
||||
const match = text.match(/Priority: (High|Medium|Low)/i);
|
||||
return match ? match[1].toLowerCase() : 'medium';
|
||||
}
|
||||
|
||||
extractImprovement(text) {
|
||||
const match = text.match(/Estimated Improvement: ([\d.]+)%/i);
|
||||
return match ? parseFloat(match[1]) : null;
|
||||
}
|
||||
|
||||
extractTimeWindows(text) {
|
||||
const windows = [];
|
||||
const matches = text.matchAll(/(\d{1,2}:\d{2}\s*[AP]M)\s*-\s*(\d{1,2}:\d{2}\s*[AP]M)/gi);
|
||||
|
||||
for (const match of matches) {
|
||||
windows.push({
|
||||
start: match[1],
|
||||
end: match[2]
|
||||
});
|
||||
}
|
||||
|
||||
return windows;
|
||||
}
|
||||
|
||||
extractDayRecommendations(text) {
|
||||
const days = [];
|
||||
const dayNames = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||||
|
||||
dayNames.forEach(day => {
|
||||
if (text.includes(day)) {
|
||||
days.push(day);
|
||||
}
|
||||
});
|
||||
|
||||
return days;
|
||||
}
|
||||
|
||||
extractTimeZoneStrategies(text) {
|
||||
// Extract time zone specific strategies
|
||||
const strategies = {};
|
||||
const matches = text.matchAll(/(UTC[+-]\d+|EST|PST|GMT).*?:(.*?)(?=UTC|EST|PST|GMT|$)/gis);
|
||||
|
||||
for (const match of matches) {
|
||||
strategies[match[1]] = match[2].trim();
|
||||
}
|
||||
|
||||
return strategies;
|
||||
}
|
||||
|
||||
extractAvoidPeriods(text) {
|
||||
const avoidMatch = text.match(/Avoid.*?:(.*?)(?=\n\n|$)/is);
|
||||
return avoidMatch ? avoidMatch[1].trim().split(',').map(p => p.trim()) : [];
|
||||
}
|
||||
|
||||
extractMessageTypeTimings(text) {
|
||||
const timings = {};
|
||||
const types = ['promotional', 'transactional', 'educational', 'reminder'];
|
||||
|
||||
types.forEach(type => {
|
||||
const match = text.match(new RegExp(`${type}.*?:\\s*([^\\n]+)`, 'i'));
|
||||
if (match) {
|
||||
timings[type] = match[1].trim();
|
||||
}
|
||||
});
|
||||
|
||||
return timings;
|
||||
}
|
||||
|
||||
extractTestSequence(text) {
|
||||
const sequenceMatch = text.match(/Test Sequence:(.*?)(?=\n\n|$)/is);
|
||||
return sequenceMatch ? sequenceMatch[1].trim().split(/\d+\./).filter(s => s.trim()) : [];
|
||||
}
|
||||
|
||||
extractTestStrategy(text) {
|
||||
const strategyMatch = text.match(/Overall Strategy:(.*?)(?=\n\n|$)/is);
|
||||
return strategyMatch ? strategyMatch[1].trim() : '';
|
||||
}
|
||||
|
||||
extractRegionalIssues(text) {
|
||||
const issues = {};
|
||||
const regions = ['EU', 'US', 'UK', 'Canada', 'Australia'];
|
||||
|
||||
regions.forEach(region => {
|
||||
const match = text.match(new RegExp(`${region}.*?:([^\\n]+)`, 'i'));
|
||||
if (match && match[1].toLowerCase().includes('issue')) {
|
||||
issues[region] = match[1].trim();
|
||||
}
|
||||
});
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
extractComplianceRecommendations(text) {
|
||||
const recommendationsMatch = text.match(/Recommendations:(.*?)(?=Required Changes:|$)/is);
|
||||
return recommendationsMatch ?
|
||||
recommendationsMatch[1].trim().split(/\d+\./).filter(r => r.trim()).map(r => r.trim()) : [];
|
||||
}
|
||||
|
||||
extractRequiredChanges(text) {
|
||||
const changesMatch = text.match(/Required Changes:(.*?)$/is);
|
||||
return changesMatch ?
|
||||
changesMatch[1].trim().split(/\d+\./).filter(c => c.trim()).map(c => c.trim()) : [];
|
||||
}
|
||||
|
||||
extractQuickWins(text) {
|
||||
const quickWinsMatch = text.match(/Quick Wins:(.*?)(?=Medium-term|$)/is);
|
||||
return quickWinsMatch ?
|
||||
quickWinsMatch[1].trim().split(/\d+\./).filter(w => w.trim()).map(w => w.trim()) : [];
|
||||
}
|
||||
|
||||
extractMediumTermStrategies(text) {
|
||||
const mediumMatch = text.match(/Medium-term.*?:(.*?)(?=Long-term|$)/is);
|
||||
return mediumMatch ?
|
||||
mediumMatch[1].trim().split(/\d+\./).filter(s => s.trim()).map(s => s.trim()) : [];
|
||||
}
|
||||
|
||||
extractLongTermPlan(text) {
|
||||
const longTermMatch = text.match(/Long-term.*?:(.*?)(?=Interactive|$)/is);
|
||||
return longTermMatch ?
|
||||
longTermMatch[1].trim().split(/\d+\./).filter(p => p.trim()).map(p => p.trim()) : [];
|
||||
}
|
||||
|
||||
extractInteractiveElements(text) {
|
||||
const interactiveMatch = text.match(/Interactive.*?:(.*?)(?=Community|$)/is);
|
||||
return interactiveMatch ?
|
||||
interactiveMatch[1].trim().split(/\d+\./).filter(e => e.trim()).map(e => e.trim()) : [];
|
||||
}
|
||||
|
||||
extractCommunityBuilding(text) {
|
||||
const communityMatch = text.match(/Community.*?:(.*?)(?=Retention|$)/is);
|
||||
return communityMatch ?
|
||||
communityMatch[1].trim().split(/\d+\./).filter(c => c.trim()).map(c => c.trim()) : [];
|
||||
}
|
||||
|
||||
extractRetentionMethods(text) {
|
||||
const retentionMatch = text.match(/Retention.*?:(.*?)$/is);
|
||||
return retentionMatch ?
|
||||
retentionMatch[1].trim().split(/\d+\./).filter(m => m.trim()).map(m => m.trim()) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache analysis results
|
||||
*/
|
||||
async cacheAnalysis(campaignId, analysis) {
|
||||
const key = `ai:analysis:${campaignId}`;
|
||||
await cache.set(key, JSON.stringify({
|
||||
analysis,
|
||||
timestamp: new Date().toISOString()
|
||||
}), 'EX', 3600); // Cache for 1 hour
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached analysis
|
||||
*/
|
||||
async getCachedAnalysis(campaignId) {
|
||||
const key = `ai:analysis:${campaignId}`;
|
||||
const cached = await cache.get(key);
|
||||
|
||||
if (cached) {
|
||||
return JSON.parse(cached);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const marketingIntelligence = new MarketingIntelligence();
|
||||
@@ -0,0 +1,276 @@
|
||||
import { logger } from '../utils/logger.js';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
export class PromptManager {
|
||||
constructor() {
|
||||
this.prompts = new Map();
|
||||
this.templates = new Map();
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (!PromptManager.instance) {
|
||||
PromptManager.instance = new PromptManager();
|
||||
PromptManager.instance.initialize();
|
||||
}
|
||||
return PromptManager.instance;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
// Load built-in prompts
|
||||
this.loadBuiltInPrompts();
|
||||
|
||||
// Load custom prompts from files if exist
|
||||
try {
|
||||
await this.loadCustomPrompts();
|
||||
} catch (error) {
|
||||
logger.warn('No custom prompts found, using built-in prompts only');
|
||||
}
|
||||
|
||||
logger.info(`Prompt manager initialized with ${this.prompts.size} prompts`);
|
||||
}
|
||||
|
||||
loadBuiltInPrompts() {
|
||||
// Campaign strategy prompt
|
||||
this.prompts.set('campaign_strategy', {
|
||||
template: `You are an expert marketing strategist specializing in Telegram campaigns.
|
||||
|
||||
Given the following campaign parameters:
|
||||
- Goals: {{goals}}
|
||||
- Target Audience: {{targetAudience}}
|
||||
- Budget: ${{budget}}
|
||||
- Duration: {{duration}}
|
||||
|
||||
Please create a comprehensive campaign strategy that includes:
|
||||
|
||||
1. **Executive Summary**: Brief overview of the strategy
|
||||
2. **Audience Analysis**: Deep dive into target audience segments
|
||||
3. **Content Strategy**: Types of content, messaging themes, and creative direction
|
||||
4. **Channel Strategy**: How to leverage Telegram features (groups, channels, bots)
|
||||
5. **Engagement Tactics**: Specific tactics to drive engagement
|
||||
6. **Budget Allocation**: How to distribute the budget across activities
|
||||
7. **Timeline**: Phased approach with milestones
|
||||
8. **KPIs and Success Metrics**: How to measure success
|
||||
9. **Risk Mitigation**: Potential challenges and solutions
|
||||
|
||||
Use the available functions to:
|
||||
- analyze_audience: Get detailed audience insights
|
||||
- generate_content_ideas: Create specific content recommendations
|
||||
- plan_schedule: Develop optimal posting schedule
|
||||
- estimate_results: Project campaign outcomes
|
||||
|
||||
Format your response as a structured JSON object.`,
|
||||
variables: ['goals', 'targetAudience', 'budget', 'duration']
|
||||
});
|
||||
|
||||
// Message analysis prompt
|
||||
this.prompts.set('message_analysis', {
|
||||
template: `Analyze the following message for marketing campaign use:
|
||||
|
||||
Content: {{content}}
|
||||
Context: {{context}}
|
||||
Intended Purpose: {{intent}}
|
||||
|
||||
Please provide:
|
||||
1. Safety assessment (use check_content_safety function)
|
||||
2. Sentiment analysis (use analyze_sentiment function)
|
||||
3. Key entities and topics (use extract_entities function)
|
||||
4. Engagement potential (score 1-10)
|
||||
5. Improvement suggestions
|
||||
6. Platform-specific considerations
|
||||
|
||||
Return a structured analysis with actionable insights.`,
|
||||
variables: ['content', 'context', 'intent']
|
||||
});
|
||||
|
||||
// Content optimization prompt
|
||||
this.prompts.set('content_optimization', {
|
||||
template: `Optimize the following content for maximum engagement:
|
||||
|
||||
Original Content: {{content}}
|
||||
|
||||
Optimization Criteria:
|
||||
{{criteria}}
|
||||
|
||||
Please provide:
|
||||
1. Optimized version of the content
|
||||
2. Key changes made and rationale
|
||||
3. Expected improvement in engagement
|
||||
4. A/B testing recommendations
|
||||
5. Platform-specific variations
|
||||
|
||||
Ensure the optimized content:
|
||||
- Maintains the core message
|
||||
- Follows platform best practices
|
||||
- Is culturally appropriate
|
||||
- Maximizes engagement potential`,
|
||||
variables: ['content', 'criteria']
|
||||
});
|
||||
|
||||
// A/B test design prompt
|
||||
this.prompts.set('ab_test_design', {
|
||||
template: `Design an A/B test for the following campaign element:
|
||||
|
||||
Element Type: {{elementType}}
|
||||
Current Version: {{currentVersion}}
|
||||
Hypothesis: {{hypothesis}}
|
||||
Success Metric: {{successMetric}}
|
||||
|
||||
Create:
|
||||
1. Test variations (2-3 alternatives)
|
||||
2. Statistical parameters (sample size, duration, confidence level)
|
||||
3. Implementation plan
|
||||
4. Analysis framework
|
||||
5. Decision criteria
|
||||
|
||||
Ensure the test is:
|
||||
- Statistically valid
|
||||
- Practically implementable
|
||||
- Aligned with campaign goals`,
|
||||
variables: ['elementType', 'currentVersion', 'hypothesis', 'successMetric']
|
||||
});
|
||||
|
||||
// Audience segmentation prompt
|
||||
this.prompts.set('audience_segmentation', {
|
||||
template: `Create detailed audience segments for targeting:
|
||||
|
||||
Available Data:
|
||||
{{audienceData}}
|
||||
|
||||
Campaign Objectives:
|
||||
{{objectives}}
|
||||
|
||||
Please provide:
|
||||
1. Segment definitions (3-5 segments)
|
||||
2. Size estimates for each segment
|
||||
3. Behavioral characteristics
|
||||
4. Messaging preferences
|
||||
5. Optimal engagement times
|
||||
6. Content preferences
|
||||
7. Conversion likelihood
|
||||
|
||||
Format as actionable targeting criteria.`,
|
||||
variables: ['audienceData', 'objectives']
|
||||
});
|
||||
|
||||
// Performance analysis prompt
|
||||
this.prompts.set('performance_analysis', {
|
||||
template: `Analyze campaign performance and provide optimization recommendations:
|
||||
|
||||
Performance Data:
|
||||
{{performanceData}}
|
||||
|
||||
Campaign Goals:
|
||||
{{goals}}
|
||||
|
||||
Time Period: {{timePeriod}}
|
||||
|
||||
Provide:
|
||||
1. Performance summary against goals
|
||||
2. Key insights and trends
|
||||
3. Bottleneck identification
|
||||
4. Optimization opportunities (ranked by impact)
|
||||
5. Specific action items
|
||||
6. Resource reallocation recommendations
|
||||
|
||||
Use data-driven insights to support all recommendations.`,
|
||||
variables: ['performanceData', 'goals', 'timePeriod']
|
||||
});
|
||||
}
|
||||
|
||||
async loadCustomPrompts() {
|
||||
const promptsDir = path.join(process.cwd(), 'prompts');
|
||||
const files = await fs.readdir(promptsDir);
|
||||
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.json')) {
|
||||
const content = await fs.readFile(path.join(promptsDir, file), 'utf-8');
|
||||
const promptData = JSON.parse(content);
|
||||
|
||||
const promptName = path.basename(file, '.json');
|
||||
this.prompts.set(promptName, promptData);
|
||||
|
||||
logger.info(`Loaded custom prompt: ${promptName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getPrompt(name, variables = {}) {
|
||||
const promptData = this.prompts.get(name);
|
||||
|
||||
if (!promptData) {
|
||||
throw new Error(`Prompt not found: ${name}`);
|
||||
}
|
||||
|
||||
// Replace variables in template
|
||||
let prompt = promptData.template;
|
||||
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
const placeholder = `{{${key}}}`;
|
||||
const replacement = typeof value === 'object' ?
|
||||
JSON.stringify(value, null, 2) :
|
||||
String(value);
|
||||
|
||||
prompt = prompt.replace(new RegExp(placeholder, 'g'), replacement);
|
||||
}
|
||||
|
||||
// Check for missing variables
|
||||
const missingVars = promptData.variables.filter(
|
||||
varName => !Object.keys(variables).includes(varName)
|
||||
);
|
||||
|
||||
if (missingVars.length > 0) {
|
||||
logger.warn(`Missing variables for prompt ${name}: ${missingVars.join(', ')}`);
|
||||
}
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
registerPrompt(name, template, variables = []) {
|
||||
this.prompts.set(name, { template, variables });
|
||||
logger.info(`Prompt registered: ${name}`);
|
||||
}
|
||||
|
||||
getAllPrompts() {
|
||||
return Array.from(this.prompts.keys());
|
||||
}
|
||||
|
||||
async savePrompt(name, promptData) {
|
||||
const promptsDir = path.join(process.cwd(), 'prompts');
|
||||
|
||||
// Ensure directory exists
|
||||
await fs.mkdir(promptsDir, { recursive: true });
|
||||
|
||||
// Save prompt to file
|
||||
const filePath = path.join(promptsDir, `${name}.json`);
|
||||
await fs.writeFile(filePath, JSON.stringify(promptData, null, 2));
|
||||
|
||||
// Update in-memory storage
|
||||
this.prompts.set(name, promptData);
|
||||
|
||||
logger.info(`Prompt saved: ${name}`);
|
||||
}
|
||||
|
||||
// Prompt versioning support
|
||||
async getPromptVersion(name, version) {
|
||||
const versionedName = `${name}_v${version}`;
|
||||
return await this.getPrompt(versionedName);
|
||||
}
|
||||
|
||||
// Dynamic prompt composition
|
||||
composePrompt(components) {
|
||||
const composed = components.map(comp => {
|
||||
if (typeof comp === 'string') {
|
||||
return comp;
|
||||
} else if (comp.type === 'prompt') {
|
||||
const prompt = this.prompts.get(comp.name);
|
||||
return prompt ? prompt.template : '';
|
||||
} else if (comp.type === 'conditional') {
|
||||
return comp.condition ? comp.ifTrue : comp.ifFalse;
|
||||
}
|
||||
return '';
|
||||
}).join('\n\n');
|
||||
|
||||
return composed;
|
||||
}
|
||||
}
|
||||
56
marketing-agent/services/claude-agent/src/utils/logger.js
Normal file
56
marketing-agent/services/claude-agent/src/utils/logger.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import winston from 'winston';
|
||||
|
||||
const { combine, timestamp, printf, colorize, errors } = winston.format;
|
||||
|
||||
// Custom log format
|
||||
const logFormat = printf(({ level, message, timestamp, stack, ...metadata }) => {
|
||||
let msg = `${timestamp} [${level}] ${message}`;
|
||||
|
||||
if (stack) {
|
||||
msg += `\n${stack}`;
|
||||
}
|
||||
|
||||
if (Object.keys(metadata).length > 0) {
|
||||
msg += ` ${JSON.stringify(metadata)}`;
|
||||
}
|
||||
|
||||
return msg;
|
||||
});
|
||||
|
||||
// Create logger instance
|
||||
export const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: combine(
|
||||
errors({ stack: true }),
|
||||
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
logFormat
|
||||
),
|
||||
transports: [
|
||||
// Console transport
|
||||
new winston.transports.Console({
|
||||
format: combine(
|
||||
colorize(),
|
||||
logFormat
|
||||
)
|
||||
}),
|
||||
// File transport for errors
|
||||
new winston.transports.File({
|
||||
filename: 'logs/error.log',
|
||||
level: 'error',
|
||||
maxsize: 10485760, // 10MB
|
||||
maxFiles: 5
|
||||
}),
|
||||
// File transport for all logs
|
||||
new winston.transports.File({
|
||||
filename: 'logs/combined.log',
|
||||
maxsize: 10485760, // 10MB
|
||||
maxFiles: 10
|
||||
})
|
||||
],
|
||||
exceptionHandlers: [
|
||||
new winston.transports.File({ filename: 'logs/exceptions.log' })
|
||||
],
|
||||
rejectionHandlers: [
|
||||
new winston.transports.File({ filename: 'logs/rejections.log' })
|
||||
]
|
||||
});
|
||||
Reference in New Issue
Block a user