feat: LinkedIn LLM alignment - Phase 1-3 complete

Phase 1: Dead Code Cleanup
- Remove GeminiGroundedProvider import and property from linkedin_service.py
- Remove fallback_provider property (gemini_provider imports)
- Fix routers/linkedin.py edit endpoint to use llm_text_gen
- Delete dead LinkedInImageEditor class
- Remove dead _transform_gemini_sources from content_generator.py

Phase 2: Research Infrastructure Alignment
- Add user_id to _conduct_research() for pre-flight validation
- Add validate_exa_research_operations() before Exa/Tavily calls
- Pass user_id to provider.simple_search() for usage tracking
- Inject research content into LLM prompts via _build_research_context()
- Fix Google engine path to fallback to Exa
- Add Exa → Tavily fallback on research failure

Phase 3: Cosmetic Cleanup
- Rename _generate_prompts_with_gemini → _generate_prompts_with_llm
- Rename _build_gemini_prompt → _build_image_prompt
- Rename _parse_gemini_response → _parse_llm_response
- Remove all Gemini references from LinkedIn code (0 remaining)
- Update docstrings and log messages

Additional:
- Research caching using existing ResearchCache
- Shared ExaContentResearchProvider in services/research/
- Persona service uses llm_text_gen instead of gemini_structured_json_response
- LinkedInWriter.tsx ChatMessage → ChatMsg type mapping fix
- RegisterLinkedInActionsEnhanced.tsx content_format_rules typing fix
This commit is contained in:
ajaysi
2026-06-12 18:58:53 +05:30
parent e54aaa7a3e
commit 63a0df2536
37 changed files with 2891 additions and 1355 deletions

View File

@@ -1,160 +1,220 @@
import React from 'react';
import { useCopilotAction } from '@copilotkit/react-core';
import { showToastNotification } from '../../utils/toastNotifications';
import { linkedInWriterApi } from '../../services/linkedInWriterApi';
import { useCopilotActionTyped } from '../../hooks/useCopilotActionTyped';
const useCopilotActionTyped = useCopilotAction as any;
function extractHashtags(text: string): string[] {
return text.match(/#[A-Za-z0-9_]+/g) || [];
}
function stripHashtags(text: string): string {
return text.replace(/#[A-Za-z0-9_]+\s*/g, '').trim();
}
const RegisterLinkedInEditActions: React.FC = () => {
// Professionalize Content
// ── 1. Professionalize ────────────────────────────────────────────────
useCopilotActionTyped({
name: 'professionalizeLinkedInContent',
description: 'Make LinkedIn content more professional and industry-appropriate',
description: 'Make LinkedIn content more professional, polished, and industry-appropriate using AI',
parameters: [
{ name: 'content', type: 'string', required: false },
{ name: 'industry', type: 'string', required: false },
{ name: 'target_audience', type: 'string', required: false }
],
handler: async (args: any) => {
// This would integrate with a backend endpoint for content professionalization
const content = args?.content || '';
const industry = args?.industry || 'Technology';
const targetAudience = args?.target_audience || 'Professionals';
// For now, return a placeholder response
const professionalizedContent = `[Professionalized version of your content for ${industry} industry targeting ${targetAudience}]\n\n${content}`;
window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: professionalizedContent } }));
return { success: true, content: professionalizedContent };
if (!content.trim()) return { success: false, message: 'No content to professionalize' };
const res = await linkedInWriterApi.editContent({
content,
edit_type: 'professionalize',
industry: args?.industry,
target_audience: args?.target_audience,
});
if (res.success && res.content) {
window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: res.content } }));
return { success: true, content: res.content, message: 'Content professionalized with AI.' };
}
return { success: false, message: res.error || 'Failed to professionalize content' };
}
});
// Optimize for Engagement
// ── 2. Optimize Engagement ────────────────────────────────────────────
useCopilotActionTyped({
name: 'optimizeLinkedInEngagement',
description: 'Optimize LinkedIn content for better engagement and reach',
parameters: [
{ name: 'content', type: 'string', required: false },
{ name: 'content_type', type: 'string', required: false }
],
handler: async (args: any) => {
const content = args?.content || '';
const contentType = args?.content_type || 'post';
// Placeholder for engagement optimization
const optimizedContent = `[Engagement-optimized ${contentType}]\n\n${content}\n\n#ProfessionalDevelopment #Networking #IndustryInsights`;
window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: optimizedContent } }));
return { success: true, content: optimizedContent };
}
});
// Add Professional Hashtags
useCopilotActionTyped({
name: 'addLinkedInHashtags',
description: 'Add relevant professional hashtags to LinkedIn content',
description: 'Optimize LinkedIn content for better engagement — strengthen hook, improve readability, encourage interaction',
parameters: [
{ name: 'content', type: 'string', required: false },
{ name: 'industry', type: 'string', required: false }
],
handler: async (args: any) => {
const content = args?.content || '';
// Placeholder for hashtag addition
const hashtags = '#ProfessionalDevelopment #Networking #IndustryInsights #CareerGrowth';
const contentWithHashtags = `${content}\n\n${hashtags}`;
window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: contentWithHashtags } }));
return { success: true, content: contentWithHashtags };
if (!content.trim()) return { success: false, message: 'No content to optimize' };
const res = await linkedInWriterApi.editContent({
content,
edit_type: 'optimize_engagement',
industry: args?.industry,
});
if (res.success && res.content) {
window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: res.content } }));
return { success: true, content: res.content, message: 'Content optimized for engagement.' };
}
return { success: false, message: res.error || 'Failed to optimize content' };
}
});
// Adjust Tone
// ── 3. Add Hashtags (AI-powered) ──────────────────────────────────────
useCopilotActionTyped({
name: 'adjustLinkedInTone',
description: 'Adjust the tone of LinkedIn content to be more professional, conversational, or authoritative',
name: 'addLinkedInHashtags',
description: 'Generate relevant, industry-specific hashtags for LinkedIn content using AI',
parameters: [
{ name: 'content', type: 'string', required: false },
{ name: 'target_tone', type: 'string', required: false }
{ name: 'industry', type: 'string', required: false }
],
handler: async (args: any) => {
const content = args?.content || '';
if (!content.trim()) return { success: false, message: 'No content to add hashtags to' };
const existingHashtags = extractHashtags(content);
if (existingHashtags.length >= 5) {
showToastNotification('Content already has plenty of hashtags.', 'info');
return { success: false, message: 'Content already has 5+ hashtags' };
}
const res = await linkedInWriterApi.editContent({
content: stripHashtags(content),
edit_type: 'add_hashtags',
industry: args?.industry,
});
if (res.success && res.content) {
window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: res.content } }));
const newHashtags = extractHashtags(res.content);
return { success: true, content: res.content, hashtags: newHashtags };
}
return { success: false, message: res.error || 'Failed to generate hashtags' };
}
});
// ── 4. Adjust Tone ────────────────────────────────────────────────────
useCopilotActionTyped({
name: 'adjustLinkedInTone',
description: 'Rewrite LinkedIn content in a different tone — professional, conversational, authoritative, educational, or friendly',
parameters: [
{ name: 'content', type: 'string', required: false },
{ name: 'target_tone', type: 'string', required: false, description: 'professional, conversational, authoritative, educational, friendly' }
],
handler: async (args: any) => {
const content = args?.content || '';
const targetTone = args?.target_tone || 'professional';
// Placeholder for tone adjustment
const adjustedContent = `[Content adjusted to ${targetTone} tone]\n\n${content}`;
window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: adjustedContent } }));
return { success: true, content: adjustedContent };
if (!content.trim()) return { success: false, message: 'No content to adjust tone for' };
const res = await linkedInWriterApi.editContent({
content,
edit_type: 'adjust_tone',
tone: targetTone,
});
if (res.success && res.content) {
window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: res.content } }));
return { success: true, content: res.content, message: `Tone adjusted to ${targetTone}.` };
}
return { success: false, message: res.error || 'Failed to adjust tone' };
}
});
// Expand Content
// ── 5. Expand Content ─────────────────────────────────────────────────
useCopilotActionTyped({
name: 'expandLinkedInContent',
description: 'Expand LinkedIn content with more details and insights',
description: 'Expand LinkedIn content with more depth, examples, data points, and actionable insights using AI',
parameters: [
{ name: 'content', type: 'string', required: false },
{ name: 'expansion_type', type: 'string', required: false }
{ name: 'industry', type: 'string', required: false },
{ name: 'target_audience', type: 'string', required: false }
],
handler: async (args: any) => {
const content = args?.content || '';
const expansionType = args?.expansion_type || 'insights';
// Placeholder for content expansion
const expandedContent = `${content}\n\n[Additional ${expansionType} and context added here]`;
window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: expandedContent } }));
return { success: true, content: expandedContent };
if (!content.trim()) return { success: false, message: 'No content to expand' };
const res = await linkedInWriterApi.editContent({
content,
edit_type: 'expand',
industry: args?.industry,
target_audience: args?.target_audience,
});
if (res.success && res.content) {
window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: res.content } }));
return { success: true, content: res.content, message: 'Content expanded with AI.' };
}
return { success: false, message: res.error || 'Failed to expand content' };
}
});
// Condense Content
// ── 6. Condense Content ───────────────────────────────────────────────
useCopilotActionTyped({
name: 'condenseLinkedInContent',
description: 'Condense LinkedIn content to be more concise and impactful',
description: 'Condense LinkedIn content to be more concise and impactful using AI — preserves key messages',
parameters: [
{ name: 'content', type: 'string', required: false },
{ name: 'target_length', type: 'string', required: false }
{ name: 'target_length', type: 'string', required: false, description: 'short, medium, long' }
],
handler: async (args: any) => {
const content = args?.content || '';
const targetLength = args?.target_length || 'short';
// Placeholder for content condensation
const condensedContent = `[Condensed to ${targetLength} format]\n\n${content.substring(0, Math.min(content.length, 500))}...`;
window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: condensedContent } }));
return { success: true, content: condensedContent };
const targetLength = args?.target_length || 'medium';
if (!content.trim()) return { success: false, message: 'No content to condense' };
const lengthMap: Record<string, string> = { short: 'very concise (1-2 sentences)', medium: 'half the original length', long: 'slightly shortened' };
const res = await linkedInWriterApi.editContent({
content,
edit_type: 'condense',
parameters: { target_length: lengthMap[targetLength] || lengthMap.medium },
});
if (res.success && res.content) {
window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: res.content } }));
return { success: true, content: res.content, message: 'Content condensed with AI.' };
}
return { success: false, message: res.error || 'Failed to condense content' };
}
});
// Add Call to Action
// ── 7. Add Call to Action ─────────────────────────────────────────────
useCopilotActionTyped({
name: 'addLinkedInCallToAction',
description: 'Add a professional call to action to LinkedIn content',
description: 'Add a contextual, engaging call-to-action to LinkedIn content using AI',
parameters: [
{ name: 'content', type: 'string', required: false },
{ name: 'cta_type', type: 'string', required: false }
{ name: 'cta_type', type: 'string', required: false, description: 'engagement, networking, learning, collaboration' }
],
handler: async (args: any) => {
const content = args?.content || '';
const ctaType = args?.cta_type || 'engagement';
const ctaOptions = {
engagement: 'What are your thoughts on this? Share your experience in the comments below!',
networking: 'Let\'s connect if you\'re interested in discussing this further.',
learning: 'Would you like to learn more about this topic? Drop a comment or DM me.',
collaboration: 'Interested in collaborating on similar projects? Let\'s connect!'
};
const cta = ctaOptions[ctaType as keyof typeof ctaOptions] || ctaOptions.engagement;
const contentWithCTA = `${content}\n\n${cta}`;
window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: contentWithCTA } }));
return { success: true, content: contentWithCTA };
if (!content.trim()) return { success: false, message: 'No content to add CTA to' };
if (/\b(call now|sign up|join|try|learn more|comment|share|connect|message|dm|reach out)\b/i.test(content)) {
showToastNotification('Content already contains a call to action.', 'info');
return { success: false, message: 'Content already has a CTA' };
}
const res = await linkedInWriterApi.editContent({
content,
edit_type: 'add_cta',
parameters: { cta_type: args?.cta_type || 'engagement' },
});
if (res.success && res.content) {
window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: res.content } }));
return { success: true, content: res.content, message: 'CTA added with AI.' };
}
return { success: false, message: res.error || 'Failed to add CTA' };
}
});
return null;
};
export default RegisterLinkedInEditActions;
export default RegisterLinkedInEditActions;