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

@@ -36,6 +36,7 @@ class SearchEngine(str, Enum):
METAPHOR = "metaphor"
GOOGLE = "google"
TAVILY = "tavily"
EXA = "exa"
class GroundingLevel(str, Enum):
@@ -57,7 +58,7 @@ class LinkedInPostRequest(BaseModel):
include_hashtags: bool = Field(default=True, description="Whether to include hashtags")
include_call_to_action: bool = Field(default=True, description="Whether to include call to action")
research_enabled: bool = Field(default=True, description="Whether to include research-backed content")
search_engine: SearchEngine = Field(default=SearchEngine.GOOGLE, description="Search engine for research")
search_engine: SearchEngine = Field(default=SearchEngine.EXA, description="Search engine for research")
max_length: int = Field(default=3000, description="Maximum character count", ge=100, le=3000)
grounding_level: GroundingLevel = Field(default=GroundingLevel.ENHANCED, description="Level of content grounding")
include_citations: bool = Field(default=True, description="Whether to include inline citations")
@@ -94,7 +95,7 @@ class LinkedInArticleRequest(BaseModel):
include_images: bool = Field(default=True, description="Whether to generate image suggestions")
seo_optimization: bool = Field(default=True, description="Whether to include SEO optimization")
research_enabled: bool = Field(default=True, description="Whether to include research-backed content")
search_engine: SearchEngine = Field(default=SearchEngine.GOOGLE, description="Search engine for research")
search_engine: SearchEngine = Field(default=SearchEngine.EXA, description="Search engine for research")
word_count: int = Field(default=1500, description="Target word count", ge=500, le=5000)
grounding_level: GroundingLevel = Field(default=GroundingLevel.ENHANCED, description="Level of content grounding")
include_citations: bool = Field(default=True, description="Whether to include inline citations")
@@ -129,9 +130,11 @@ class LinkedInCarouselRequest(BaseModel):
number_of_slides: int = Field(default=5, description="Number of slides", ge=3, le=10)
include_cover_slide: bool = Field(default=True, description="Whether to include a cover slide")
include_cta_slide: bool = Field(default=True, description="Whether to include a call-to-action slide")
key_points: Optional[List[str]] = Field(None, description="Specific key points to cover", max_items=10)
research_enabled: bool = Field(default=True, description="Whether to include research-backed content")
search_engine: SearchEngine = Field(default=SearchEngine.GOOGLE, description="Search engine for research")
search_engine: SearchEngine = Field(default=SearchEngine.EXA, description="Search engine for research")
grounding_level: GroundingLevel = Field(default=GroundingLevel.ENHANCED, description="Level of content grounding")
color_scheme: str = Field(default="professional", description="Color scheme for PDF rendering: professional, creative, industry, dark, minimal")
include_citations: bool = Field(default=True, description="Whether to include inline citations")
class Config:
@@ -144,9 +147,11 @@ class LinkedInCarouselRequest(BaseModel):
"number_of_slides": 6,
"include_cover_slide": True,
"include_cta_slide": True,
"key_points": ["Remote collaboration tools", "Work-life balance", "Productivity metrics"],
"research_enabled": True,
"search_engine": "google",
"grounding_level": "enhanced",
"color_scheme": "professional",
"include_citations": True
}
}
@@ -161,8 +166,9 @@ class LinkedInVideoScriptRequest(BaseModel):
video_duration: int = Field(default=60, description="Target video duration in seconds", ge=30, le=300)
include_captions: bool = Field(default=True, description="Whether to include captions")
include_thumbnail_suggestions: bool = Field(default=True, description="Whether to include thumbnail suggestions")
key_points: Optional[List[str]] = Field(None, description="Specific key points to cover in the video", max_items=10)
research_enabled: bool = Field(default=True, description="Whether to include research-backed content")
search_engine: SearchEngine = Field(default=SearchEngine.GOOGLE, description="Search engine for research")
search_engine: SearchEngine = Field(default=SearchEngine.EXA, description="Search engine for research")
grounding_level: GroundingLevel = Field(default=GroundingLevel.ENHANCED, description="Level of content grounding")
include_citations: bool = Field(default=True, description="Whether to include inline citations")
@@ -176,6 +182,7 @@ class LinkedInVideoScriptRequest(BaseModel):
"video_duration": 90,
"include_captions": True,
"include_thumbnail_suggestions": True,
"key_points": ["Zero trust architecture", "Phishing prevention", "Incident response"],
"research_enabled": True,
"search_engine": "google",
"grounding_level": "enhanced",
@@ -193,7 +200,7 @@ class LinkedInCommentResponseRequest(BaseModel):
response_length: str = Field(default="medium", description="Length of response: short, medium, long")
include_questions: bool = Field(default=True, description="Whether to include engaging questions")
research_enabled: bool = Field(default=False, description="Whether to include research-backed content")
search_engine: SearchEngine = Field(default=SearchEngine.GOOGLE, description="Search engine for research")
search_engine: SearchEngine = Field(default=SearchEngine.EXA, description="Search engine for research")
grounding_level: GroundingLevel = Field(default=GroundingLevel.BASIC, description="Level of content grounding")
class Config:
@@ -451,4 +458,24 @@ class LinkedInCommentResponseResult(BaseModel):
tone_analysis: Optional[Dict[str, Any]] = None
generation_metadata: Dict[str, Any] = {}
error: Optional[str] = None
grounding_status: Optional[Dict[str, Any]] = Field(None, description="Grounding operation status")
grounding_status: Optional[Dict[str, Any]] = Field(None, description="Grounding operation status")
class LinkedInEditContentRequest(BaseModel):
"""Request model for AI-powered LinkedIn content editing."""
content: str = Field(..., description="Content to edit", min_length=1)
edit_type: str = Field(..., description="Type of edit: professionalize, optimize_engagement, add_hashtags, adjust_tone, expand, condense, add_cta")
industry: Optional[str] = Field(None, description="Industry context for the edit")
tone: Optional[str] = Field(None, description="Target tone: professional, conversational, authoritative, educational, friendly")
target_audience: Optional[str] = Field(None, description="Target audience for the content")
parameters: Optional[Dict[str, Any]] = Field(None, description="Additional parameters specific to edit type")
class LinkedInEditContentResponse(BaseModel):
"""Response model for AI-powered LinkedIn content editing."""
success: bool = True
content: Optional[str] = None
edit_type: str
provider: Optional[str] = None
model: Optional[str] = None
error: Optional[str] = None