diff --git a/src/generateTitle.ts b/src/generateTitle.ts index b50d676..9546b9a 100644 --- a/src/generateTitle.ts +++ b/src/generateTitle.ts @@ -14,11 +14,25 @@ const TitleSchema = z.object({ title: z.string().min(5, "Title must be at least 5 characters").max(100, "Title must not exceed 100 characters") }); +// Sentiment summaries result type +export type SentimentSummaries = { + bullishSummary?: string; + bearishSummary?: string; + neutralSummary?: string; +}; + +// Zod schema for sentiment summaries validation +const SentimentSummariesSchema = z.object({ + bullishSummary: z.string().min(10).max(500).optional(), + bearishSummary: z.string().min(10).max(500).optional(), + neutralSummary: z.string().min(10).max(500).optional(), +}); + // Generate title for a single post export async function generateTitleForPost(post: string, clientOverride?: OpenAI): Promise { const usedClient: OpenAI = clientOverride ?? openai; - const systemPrompt = `You are a title generation system for social media posts, particularly crypto and tech-related content. + const systemPrompt = `You are a title generation system for social media posts, particularly finance and tech-related content. Instructions: 1. Generate a concise, engaging title that captures the essence of the post @@ -51,6 +65,57 @@ Be strict: return only raw JSON with exactly that shape; no code fences or prose } } +// Generate sentiment-based summaries for a group of posts +export async function generateSentimentSummariesForGroup(posts: any[], clientOverride?: OpenAI): Promise { + const usedClient: OpenAI = clientOverride ?? openai; + + const systemPrompt = `You are a sentiment analysis and summary generation system for social media posts, particularly finance and tech-related content. + +Instructions: +1. Analyze the provided posts and their sentiments (BULLISH, BEARISH, NEUTRAL). +2. For each sentiment (bullish, bearish, neutral) that is present in the posts, generate a summary. If there are no posts for a sentiment, do not generate or include a summary for it. +3. Each summary should be 10-500 characters. +4. Focus on the main themes, key insights, and overall sentiment trends for each sentiment present. +5. Make summaries informative and actionable, but do not make up content for sentiments not present in the posts. +6. Return only valid JSON in this format, including only the summaries for sentiments that exist in the posts: +{ + "bullishSummary"?: "Your bullish summary here (optional)", + "bearishSummary"?: "Your bearish summary here (optional)", + "neutralSummary"?: "Your neutral summary here (optional)" +} + +Be strict: return only raw JSON with exactly that shape; no code fences or prose.`; + + const postsJson = JSON.stringify(posts); + + try { + const validated = await callOpenAIWithValidation({ + client: usedClient, + systemPrompt, + userPrompt: postsJson, + schema: SentimentSummariesSchema, + retryCount: 3, + maxTokens: 800 + }); + + if (!validated?.bullishSummary && !validated?.bearishSummary && !validated?.neutralSummary) { + throw new Error(`Sentiment summaries generation failed for posts: no summaries returned.`); + } + // Only return summaries for sentiments present in the posts + return { + ...(validated.bullishSummary ? { bullishSummary: validated.bullishSummary } : {}), + ...(validated.bearishSummary ? { bearishSummary: validated.bearishSummary } : {}), + ...(validated.neutralSummary ? { neutralSummary: validated.neutralSummary } : {}) + }; + } catch (e) { + console.error("Error generating sentiment summaries.", { + postCount: Array.isArray(posts) ? posts.length : undefined, + error: e instanceof Error ? e.message : String(e) + }); + throw e; + } +} + // Generate titles for multiple posts export async function generateTitlesForPosts(posts: string[]): Promise { const results: TitleResult[] = []; diff --git a/src/openaiValidationUtil.ts b/src/openaiValidationUtil.ts index f15b9af..112fa9a 100644 --- a/src/openaiValidationUtil.ts +++ b/src/openaiValidationUtil.ts @@ -12,7 +12,8 @@ export async function callOpenAIWithValidation(params: { systemPrompt: string, userPrompt: string, schema: ZodSchema, - retryCount?: number + retryCount?: number, + maxTokens?: number }): Promise { let lastError: unknown = null; const baseDelayMs = 500; @@ -37,7 +38,7 @@ export async function callOpenAIWithValidation(params: { model: "gpt-4o-mini", messages, response_format: { type: "json_object" } as const, - max_tokens: 200 + max_tokens: params.maxTokens ?? 200 }; try { const response = await params.client.chat.completions.create(chatParams); diff --git a/src/postGroup.ts b/src/postGroup.ts index cdcb680..77dd3b3 100644 --- a/src/postGroup.ts +++ b/src/postGroup.ts @@ -1,6 +1,6 @@ import cron from 'node-cron'; -import { generateTitleForPost } from './generateTitle'; +import { generateTitleForPost, generateSentimentSummariesForGroup, type SentimentSummaries } from './generateTitle'; import { initRedis, getRedisClient } from './redisClient'; export type Post = { @@ -19,6 +19,9 @@ export type PostGroup = { id: string; posts: Post[]; title?: string; + bullishSummary?: string; + bearishSummary?: string; + neutralSummary?: string; }; // Generate a title for a PostGroup by aggregating its posts' content @@ -27,6 +30,11 @@ export async function generateTitleForPostGroup(postGroup: PostGroup): Promise { + return await generateSentimentSummariesForGroup(postGroup.posts); +} + // Fetch all PostGroups from Redis (expects a key 'PostGroup' with a JSON array, or adapt as needed) export async function fetchPostGroupsFromRedis(): Promise { @@ -45,50 +53,51 @@ export async function fetchPostGroupsFromRedis(): Promise { -// Reusable function to generate and log the title for all PostGroups from Redis +// Reusable function to generate and log the title and sentiment summaries for all PostGroups from Redis export async function logTitlesForAllPostGroups(context: 'CRON' | 'MANUAL' = 'MANUAL') { const postGroups = await fetchPostGroupsFromRedis(); if (!postGroups.length) { console.log('No PostGroups found in Redis.'); return; } - let updated = false; const postGroupsWithOrderedKeys = []; for (const group of postGroups) { - let title = group.title; try { - title = await generateTitleForPostGroup(group); - if (group.title !== title) { - updated = true; - } + // Always generate and update title + const title = await generateTitleForPostGroup(group); + // Always generate and update sentiment summaries + const summaries = await generateSentimentSummariesForPostGroup(group); + if (context === 'CRON') { console.log(`[CRON] Generated Title for PostGroup (id: ${group.id}) at ${new Date().toISOString()}:`, title); } else { console.log(`Title for PostGroup (id: ${group.id}):`, title); } + + postGroupsWithOrderedKeys.push({ + id: group.id, + title, + bullishSummary: summaries.bullishSummary, + bearishSummary: summaries.bearishSummary, + neutralSummary: summaries.neutralSummary, + posts: group.posts + }); } catch (e) { if (context === 'CRON') { - console.error(`[CRON] Error generating title for PostGroup (id: ${group.id}):`, e); + console.error(`[CRON] Error generating title/summaries for PostGroup (id: ${group.id}):`, e); } else { - console.error(`Error generating title for PostGroup (id: ${group.id}):`, e); + console.error(`Error generating title/summaries for PostGroup (id: ${group.id}):`, e); } } - postGroupsWithOrderedKeys.push({ - id: group.id, - title, - posts: group.posts - }); } - // Save updated PostGroups with titles back to Redis - if (updated) { - await initRedis(); - const redis = getRedisClient(); - await redis.set('post-groups', JSON.stringify(postGroupsWithOrderedKeys)); - if (context === 'CRON') { - console.log('[CRON] Updated post-groups with titles saved to Redis.'); - } else { - console.log('Updated post-groups with titles saved to Redis.'); - } + // Always save updated PostGroups with titles and summaries back to Redis + await initRedis(); + const redis = getRedisClient(); + await redis.set('post-groups', JSON.stringify(postGroupsWithOrderedKeys)); + if (context === 'CRON') { + console.log('[CRON] Updated post-groups with titles and sentiment summaries saved to Redis.'); + } else { + console.log('Updated post-groups with titles and sentiment summaries saved to Redis.'); } }