Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 66 additions & 1 deletion src/generateTitle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
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
Expand Down Expand Up @@ -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<SentimentSummaries> {
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);

Comment on lines +89 to +90
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Send only the minimum data needed to the LLM.

Reduce payload to content + sentiment to lower cost/PII exposure.

-    const postsJson = JSON.stringify(posts);
+    const minimal = Array.isArray(posts)
+        ? posts.map(p => ({ content: p?.content, sentiment: p?.sentiment }))
+        : [];
+    const postsJson = JSON.stringify(minimal);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const postsJson = JSON.stringify(posts);
const minimal = Array.isArray(posts)
? posts.map(p => ({ content: p?.content, sentiment: p?.sentiment }))
: [];
const postsJson = JSON.stringify(minimal);
🤖 Prompt for AI Agents
In src/generateTitle.ts around lines 89-90, the code currently serializes the
entire posts objects (const postsJson = JSON.stringify(posts)); reduce payload
by mapping posts to only the fields required by the LLM (content and sentiment)
and then JSON.stringify that reduced array before sending; update the variable
to reflect the minimal payload and ensure any downstream usage expects the
reduced shape.

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<TitleResult[]> {
const results: TitleResult[] = [];
Expand Down
5 changes: 3 additions & 2 deletions src/openaiValidationUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export async function callOpenAIWithValidation<T>(params: {
systemPrompt: string,
userPrompt: string,
schema: ZodSchema<T>,
retryCount?: number
retryCount?: number,
maxTokens?: number
}): Promise<T> {
let lastError: unknown = null;
const baseDelayMs = 500;
Expand All @@ -37,7 +38,7 @@ export async function callOpenAIWithValidation<T>(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);
Expand Down
59 changes: 34 additions & 25 deletions src/postGroup.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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
Expand All @@ -27,6 +30,11 @@ export async function generateTitleForPostGroup(postGroup: PostGroup): Promise<s
return await generateTitleForPost(combinedContent);
}

// Generate sentiment summaries for a PostGroup based on its posts
export async function generateSentimentSummariesForPostGroup(postGroup: PostGroup): Promise<SentimentSummaries> {
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<PostGroup[]> {
Expand All @@ -45,50 +53,51 @@ export async function fetchPostGroupsFromRedis(): Promise<PostGroup[]> {



// 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
});
Comment on lines +77 to +84
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Preserve all existing group fields when saving.

Current push constructs a new object and may drop unmodeled fields. Merge into the original to avoid data loss.

-            postGroupsWithOrderedKeys.push({
-                id: group.id,
-                title,
-                bullishSummary: summaries.bullishSummary,
-                bearishSummary: summaries.bearishSummary,
-                neutralSummary: summaries.neutralSummary,
-                posts: group.posts
-            });
+            const updatedGroup: PostGroup = {
+                ...group,
+                title,
+                ...summaries
+            };
+            postGroupsWithOrderedKeys.push(updatedGroup);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
postGroupsWithOrderedKeys.push({
id: group.id,
title,
bullishSummary: summaries.bullishSummary,
bearishSummary: summaries.bearishSummary,
neutralSummary: summaries.neutralSummary,
posts: group.posts
});
const updatedGroup: PostGroup = {
...group,
title,
...summaries
};
postGroupsWithOrderedKeys.push(updatedGroup);
🤖 Prompt for AI Agents
In src/postGroup.ts around lines 77 to 84, the code builds a new object literal
which can drop unmodeled fields on the original group; instead merge the
original group into the pushed object so all existing properties are preserved
and only the intended fields are overridden. Replace the explicit object
construction with a merged form (e.g. spread or Object.assign) that starts from
group and then sets title, bullishSummary, bearishSummary, neutralSummary and
posts so no original fields are lost.

} 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);
}
}
Comment on lines 85 to 91
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Do not drop groups on generation failure.

On error, the group is omitted from the saved array, effectively deleting it from Redis.

         } catch (e) {
             if (context === 'CRON') {
                 console.error(`[CRON] Error generating title/summaries for PostGroup (id: ${group.id}):`, e);
             } else {
                 console.error(`Error generating title/summaries for PostGroup (id: ${group.id}):`, e);
             }
+            // Preserve existing group to avoid data loss
+            postGroupsWithOrderedKeys.push(group);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} 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);
}
}
} catch (e) {
if (context === 'CRON') {
console.error(`[CRON] Error generating title/summaries for PostGroup (id: ${group.id}):`, e);
} else {
console.error(`Error generating title/summaries for PostGroup (id: ${group.id}):`, e);
}
// Preserve existing group to avoid data loss
postGroupsWithOrderedKeys.push(group);
}
🤖 Prompt for AI Agents
In src/postGroup.ts around lines 85 to 91, the current catch swallows generation
errors and lets the group be omitted from the saved array (effectively deleting
it from Redis); ensure the group is always preserved by adding logic to append
the original group to the saved list even when generation fails — either move
the savedGroups.push(group) into a finally block that runs regardless of
success/failure, or explicitly push the group inside the catch before logging
the error so the group is retained and saved back to Redis.

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.');
}
}

Expand Down