import { z } from 'zod';
import type { Platform, UnifiedPost, ProfileAnalysis } from '../types.js';
import { getPlatform } from '../platforms/index.js';
import { upsertPosts } from '../db/lance.js';
import { filterQuery, getAllPosts } from '../db/lance.js';
import { analyzeSentiment, analyzeSentimentBatch } from '../analysis/sentiment.js';
import { calculateBatchEngagement, getEngagementRateBenchmark } from '../analysis/engagement.js';
import { getPostingFrequency, getPeakPostingHours } from '../analysis/trends.js';
// --- Tool Schemas ---
export const analysisSchemas = {
analyze_profile: z.object({
url_or_username: z.string().describe('Profile URL or username to analyze'),
platform: z.enum(['twitter', 'instagram', 'tiktok', 'youtube', 'linkedin', 'facebook', 'reddit']).describe('Social media platform'),
max_posts: z.number().optional().describe('Number of recent posts to analyze (default 50)'),
}),
analyze_sentiment: z.object({
query: z.string().optional().describe('Search query to find posts for sentiment analysis. If omitted, analyzes all timeline data.'),
platforms: z.array(z.enum(['twitter', 'instagram', 'tiktok', 'youtube', 'linkedin', 'facebook', 'reddit'])).optional().describe('Filter by platforms'),
texts: z.array(z.string()).optional().describe('Direct text inputs to analyze (alternative to query)'),
}),
compare: z.object({
items: z.array(z.object({
name: z.string().describe('Label for this item (e.g., username, hashtag, or topic)'),
platform: z.enum(['twitter', 'instagram', 'tiktok', 'youtube', 'linkedin', 'facebook', 'reddit']).optional().describe('Platform (required for profile comparisons)'),
query: z.string().optional().describe('Search query to find relevant posts'),
})).min(2).describe('Items to compare (2 or more profiles, hashtags, or topics)'),
metric: z.enum(['engagement', 'sentiment', 'volume', 'all']).optional().describe('Comparison metric (default: all)'),
}),
};
export const analysisDescriptions: Record<string, string> = {
analyze_profile: 'Analyze a social media profile: scrape recent posts and compute engagement rate, posting frequency, content themes, peak hours, and sentiment.',
analyze_sentiment: 'Run sentiment analysis on posts. Provide a query to search timeline data, or pass texts directly. Returns positive/negative/neutral breakdown.',
compare: 'Compare 2+ profiles, hashtags, or topics side-by-side across engagement, sentiment, and volume metrics.',
};
// --- Tool Handlers ---
export const analysisHandlers = {
async analyze_profile(params: Record<string, unknown>) {
const { url_or_username, platform, max_posts } = params as {
url_or_username: string; platform: Platform; max_posts?: number;
};
const platformModule = getPlatform(platform);
const limit = max_posts ?? 50;
// Build scrape input for the profile
const scrapeParams: Record<string, unknown> = { max_results: limit };
switch (platform) {
case 'twitter':
scrapeParams.query = url_or_username;
scrapeParams.type = 'user';
break;
case 'instagram':
scrapeParams.urls = [url_or_username.startsWith('http') ? url_or_username : `https://www.instagram.com/${url_or_username}/`];
break;
case 'tiktok':
scrapeParams.profiles = [url_or_username];
break;
case 'youtube':
scrapeParams.channel_urls = [url_or_username.startsWith('http') ? url_or_username : `https://www.youtube.com/@${url_or_username}`];
break;
case 'linkedin':
scrapeParams.profile_urls = [url_or_username];
break;
case 'facebook':
scrapeParams.page_urls = [url_or_username.startsWith('http') ? url_or_username : `https://www.facebook.com/${url_or_username}`];
break;
case 'reddit':
scrapeParams.urls = [`https://www.reddit.com/user/${url_or_username}/`];
break;
}
const input = platformModule.buildInput(scrapeParams);
const posts = await platformModule.scrape(input);
// Save to timeline
try { await upsertPosts(posts); } catch { /* best-effort */ }
const engagement = calculateBatchEngagement(posts);
const benchmark = getEngagementRateBenchmark(platform);
const sentiment = analyzeSentimentBatch(posts.map(p => p.content));
const frequency = getPostingFrequency(posts);
const peakHours = getPeakPostingHours(posts);
// Top hashtags
const hashtagMap = new Map<string, number>();
for (const post of posts) {
for (const tag of post.hashtags) {
hashtagMap.set(tag, (hashtagMap.get(tag) || 0) + 1);
}
}
const topHashtags = [...hashtagMap.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([tag]) => tag);
const analysis: ProfileAnalysis = {
platform,
username: url_or_username,
postCount: posts.length,
avgEngagement: engagement.totalEngagement / Math.max(posts.length, 1),
engagementRate: engagement.rate,
postingFrequency: frequency,
topHashtags,
peakPostingHours: peakHours,
sentiment,
recentPosts: posts.slice(0, 5),
};
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
...analysis,
benchmark: {
platform,
yourRate: engagement.rate,
industryAvg: benchmark.average,
rating: engagement.rate > benchmark.high ? 'excellent'
: engagement.rate > benchmark.average ? 'above average'
: engagement.rate > benchmark.low ? 'average'
: 'below average',
},
}, null, 2),
}],
};
},
async analyze_sentiment(params: Record<string, unknown>) {
const { query, platforms, texts } = params as {
query?: string; platforms?: Platform[]; texts?: string[];
};
if (texts?.length) {
const result = analyzeSentimentBatch(texts);
const individual = texts.map(t => ({ text: t.slice(0, 100), ...analyzeSentiment(t) }));
return {
content: [{
type: 'text' as const,
text: JSON.stringify({ overall: result, individual, count: texts.length }, null, 2),
}],
};
}
let posts: UnifiedPost[];
if (query) {
posts = await filterQuery({
platforms,
limit: 200,
});
// Simple text match filter
const q = query.toLowerCase();
posts = posts.filter(p => p.content.toLowerCase().includes(q) || p.hashtags.some(h => h.includes(q)));
} else {
posts = await getAllPosts(200);
if (platforms?.length) {
posts = posts.filter(p => platforms.includes(p.platform));
}
}
const result = analyzeSentimentBatch(posts.map(p => p.content));
// Per-platform breakdown
const byPlatform: Record<string, ReturnType<typeof analyzeSentimentBatch>> = {};
const platformGroups = new Map<Platform, string[]>();
for (const post of posts) {
const group = platformGroups.get(post.platform) || [];
group.push(post.content);
platformGroups.set(post.platform, group);
}
for (const [platform, contents] of platformGroups) {
byPlatform[platform] = analyzeSentimentBatch(contents);
}
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
overall: result,
byPlatform,
postsAnalyzed: posts.length,
query: query ?? 'all timeline data',
}, null, 2),
}],
};
},
async compare(params: Record<string, unknown>) {
const { items, metric } = params as {
items: { name: string; platform?: Platform; query?: string }[];
metric?: 'engagement' | 'sentiment' | 'volume' | 'all';
};
const effectiveMetric = metric ?? 'all';
const comparisons = [];
for (const item of items) {
let posts: UnifiedPost[];
if (item.query) {
posts = await filterQuery({
platforms: item.platform ? [item.platform] : undefined,
limit: 100,
});
const q = item.query.toLowerCase();
posts = posts.filter(p => p.content.toLowerCase().includes(q) || p.hashtags.some(h => h.includes(q)));
} else {
posts = await filterQuery({
author: item.name,
platforms: item.platform ? [item.platform] : undefined,
limit: 100,
});
}
const engagement = calculateBatchEngagement(posts);
const sentiment = analyzeSentimentBatch(posts.map(p => p.content));
const result: Record<string, unknown> = { name: item.name, platform: item.platform };
if (effectiveMetric === 'all' || effectiveMetric === 'volume') {
result.volume = { totalPosts: posts.length };
}
if (effectiveMetric === 'all' || effectiveMetric === 'engagement') {
result.engagement = engagement;
}
if (effectiveMetric === 'all' || effectiveMetric === 'sentiment') {
result.sentiment = sentiment;
}
comparisons.push(result);
}
return {
content: [{
type: 'text' as const,
text: JSON.stringify({ metric: effectiveMetric, comparisons }, null, 2),
}],
};
},
};