import { z } from 'zod';
import type { Platform, TimelineStats } from '../types.js';
import { PLATFORMS } from '../types.js';
import { vectorSearch, filterQuery, getAllPosts, getPostCount } from '../db/lance.js';
import { detectTrends } from '../analysis/trends.js';
// --- Tool Schemas ---
export const timelineSchemas = {
timeline_search: z.object({
query: z.string().describe('Semantic search query — finds posts with similar meaning, not just keyword matches'),
platforms: z.array(z.enum(['twitter', 'instagram', 'tiktok', 'youtube', 'linkedin', 'facebook', 'reddit'])).optional().describe('Filter by platforms'),
date_from: z.string().optional().describe('Start date (YYYY-MM-DD)'),
date_to: z.string().optional().describe('End date (YYYY-MM-DD)'),
limit: z.number().optional().describe('Max results (default 10)'),
}),
timeline_query: z.object({
platforms: z.array(z.enum(['twitter', 'instagram', 'tiktok', 'youtube', 'linkedin', 'facebook', 'reddit'])).optional().describe('Filter by platforms'),
date_from: z.string().optional().describe('Start date (YYYY-MM-DD)'),
date_to: z.string().optional().describe('End date (YYYY-MM-DD)'),
min_engagement: z.number().optional().describe('Minimum total engagement (likes + comments + shares)'),
author: z.string().optional().describe('Filter by author username'),
hashtags: z.array(z.string()).optional().describe('Filter by hashtags'),
limit: z.number().optional().describe('Max results (default 50)'),
}),
timeline_trends: z.object({
time_window: z.enum(['hourly', 'daily', 'weekly']).optional().describe('Time bucket size for trend detection (default: daily)'),
platforms: z.array(z.enum(['twitter', 'instagram', 'tiktok', 'youtube', 'linkedin', 'facebook', 'reddit'])).optional().describe('Filter by platforms'),
top_n: z.number().optional().describe('Number of top trends to return (default 10)'),
}),
timeline_stats: z.object({
platforms: z.array(z.enum(['twitter', 'instagram', 'tiktok', 'youtube', 'linkedin', 'facebook', 'reddit'])).optional().describe('Filter by platforms'),
date_from: z.string().optional().describe('Start date (YYYY-MM-DD)'),
date_to: z.string().optional().describe('End date (YYYY-MM-DD)'),
}),
};
export const timelineDescriptions: Record<string, string> = {
timeline_search: 'Semantic search across your timeline database. Uses AI embeddings to find posts by meaning, not just keywords. Requires OPENAI_API_KEY.',
timeline_query: 'Structured query against the timeline database. Filter by platform, date, engagement, author, or hashtags — no AI needed.',
timeline_trends: 'Detect trending topics across your stored timeline data. Identifies spikes in hashtag/keyword frequency and sentiment shifts.',
timeline_stats: 'Get aggregate statistics from your timeline: total posts by platform, avg engagement, most active authors, top hashtags.',
};
// --- Tool Handlers ---
export const timelineHandlers = {
async timeline_search(params: Record<string, unknown>) {
const { query, platforms, date_from, date_to, limit } = params as {
query: string; platforms?: Platform[]; date_from?: string; date_to?: string; limit?: number;
};
const filters: string[] = [];
if (platforms?.length) {
filters.push(`platform IN (${platforms.map(p => `"${p}"`).join(',')})`);
}
if (date_from) filters.push(`timestamp >= "${date_from}"`);
if (date_to) filters.push(`timestamp <= "${date_to}"`);
const filter = filters.length > 0 ? filters.join(' AND ') : undefined;
const posts = await vectorSearch(query, limit ?? 10, filter);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({ query, results: posts, total: posts.length }, null, 2),
}],
};
},
async timeline_query(params: Record<string, unknown>) {
const posts = await filterQuery({
platforms: params.platforms as Platform[] | undefined,
dateFrom: params.date_from as string | undefined,
dateTo: params.date_to as string | undefined,
minEngagement: params.min_engagement as number | undefined,
author: params.author as string | undefined,
hashtags: params.hashtags as string[] | undefined,
limit: params.limit as number | undefined,
});
return {
content: [{
type: 'text' as const,
text: JSON.stringify({ results: posts, total: posts.length }, null, 2),
}],
};
},
async timeline_trends(params: Record<string, unknown>) {
const { time_window, platforms, top_n } = params as {
time_window?: 'hourly' | 'daily' | 'weekly'; platforms?: Platform[]; top_n?: number;
};
let posts = await getAllPosts();
if (platforms?.length) {
posts = posts.filter(p => platforms.includes(p.platform));
}
const trends = detectTrends(posts, time_window ?? 'daily', top_n ?? 10);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({ trends, totalPostsAnalyzed: posts.length }, null, 2),
}],
};
},
async timeline_stats(params: Record<string, unknown>) {
const { platforms, date_from, date_to } = params as {
platforms?: Platform[]; date_from?: string; date_to?: string;
};
let posts = await getAllPosts();
if (platforms?.length) {
posts = posts.filter(p => platforms.includes(p.platform));
}
if (date_from) {
posts = posts.filter(p => p.timestamp >= new Date(date_from));
}
if (date_to) {
posts = posts.filter(p => p.timestamp <= new Date(date_to));
}
const byPlatform: Record<string, number> = {};
const avgEngagement: Record<string, number[]> = {};
const authorCounts = new Map<string, { username: string; platform: Platform; count: number }>();
const hashtagCounts = new Map<string, number>();
for (const platform of PLATFORMS) {
byPlatform[platform] = 0;
avgEngagement[platform] = [];
}
for (const post of posts) {
byPlatform[post.platform] = (byPlatform[post.platform] || 0) + 1;
const eng = post.engagement.likes + post.engagement.comments + post.engagement.shares;
avgEngagement[post.platform]?.push(eng);
const authorKey = `${post.platform}:${post.author.username}`;
const existing = authorCounts.get(authorKey);
if (existing) {
existing.count++;
} else {
authorCounts.set(authorKey, { username: post.author.username, platform: post.platform, count: 1 });
}
for (const tag of post.hashtags) {
hashtagCounts.set(tag, (hashtagCounts.get(tag) || 0) + 1);
}
}
const avgEng: Record<string, number> = {};
for (const [platform, values] of Object.entries(avgEngagement)) {
avgEng[platform] = values.length > 0 ? Math.round(values.reduce((a, b) => a + b, 0) / values.length) : 0;
}
const timestamps = posts.map(p => p.timestamp.getTime()).filter(t => !isNaN(t));
const stats: TimelineStats = {
totalPosts: posts.length,
byPlatform: byPlatform as Record<Platform, number>,
dateRange: timestamps.length > 0 ? {
earliest: new Date(Math.min(...timestamps)),
latest: new Date(Math.max(...timestamps)),
} : null,
topAuthors: [...authorCounts.values()]
.sort((a, b) => b.count - a.count)
.slice(0, 10),
topHashtags: [...hashtagCounts.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, 20)
.map(([tag, count]) => ({ tag, count })),
avgEngagement: avgEng as Record<Platform, number>,
};
return {
content: [{
type: 'text' as const,
text: JSON.stringify(stats, null, 2),
}],
};
},
};