Added citation and quality metrics to the content editor.
This commit is contained in:
11
backend/services/linkedin/__init__.py
Normal file
11
backend/services/linkedin/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
LinkedIn Services Package
|
||||
|
||||
Contains specialized services for LinkedIn content generation.
|
||||
"""
|
||||
|
||||
from .quality_handler import QualityHandler
|
||||
from .content_generator import ContentGenerator
|
||||
from .research_handler import ResearchHandler
|
||||
|
||||
__all__ = ["QualityHandler", "ContentGenerator", "ResearchHandler"]
|
||||
748
backend/services/linkedin/content_generator.py
Normal file
748
backend/services/linkedin/content_generator.py
Normal file
@@ -0,0 +1,748 @@
|
||||
"""
|
||||
Content Generator for LinkedIn Content Generation
|
||||
|
||||
Handles the main content generation logic for posts and articles.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
from loguru import logger
|
||||
from models.linkedin_models import (
|
||||
LinkedInPostRequest, LinkedInArticleRequest, LinkedInPostResponse, LinkedInArticleResponse,
|
||||
PostContent, ArticleContent, GroundingLevel, ResearchSource
|
||||
)
|
||||
from services.linkedin.quality_handler import QualityHandler
|
||||
|
||||
|
||||
class ContentGenerator:
|
||||
"""Handles content generation for all LinkedIn content types."""
|
||||
|
||||
def __init__(self, citation_manager=None, quality_analyzer=None, gemini_grounded=None, fallback_provider=None):
|
||||
self.citation_manager = citation_manager
|
||||
self.quality_analyzer = quality_analyzer
|
||||
self.gemini_grounded = gemini_grounded
|
||||
self.fallback_provider = fallback_provider
|
||||
|
||||
def _transform_gemini_sources(self, gemini_sources):
|
||||
"""Transform Gemini sources to ResearchSource format."""
|
||||
transformed_sources = []
|
||||
for source in gemini_sources:
|
||||
transformed_source = ResearchSource(
|
||||
title=source.get('title', 'Unknown Source'),
|
||||
url=source.get('url', ''),
|
||||
content=f"Source from {source.get('title', 'Unknown')}",
|
||||
relevance_score=0.8, # Default relevance score
|
||||
credibility_score=0.7, # Default credibility score
|
||||
domain_authority=0.6, # Default domain authority
|
||||
source_type=source.get('type', 'web'),
|
||||
publication_date=datetime.now().strftime('%Y-%m-%d')
|
||||
)
|
||||
transformed_sources.append(transformed_source)
|
||||
return transformed_sources
|
||||
|
||||
async def generate_post(
|
||||
self,
|
||||
request: LinkedInPostRequest,
|
||||
research_sources: List,
|
||||
research_time: float,
|
||||
content_result: Dict[str, Any],
|
||||
grounding_enabled: bool
|
||||
) -> LinkedInPostResponse:
|
||||
"""Generate LinkedIn post with all processing steps."""
|
||||
try:
|
||||
start_time = datetime.now()
|
||||
|
||||
# Debug: Log what we received
|
||||
logger.info(f"ContentGenerator.generate_post called with:")
|
||||
logger.info(f" - research_sources count: {len(research_sources) if research_sources else 0}")
|
||||
logger.info(f" - research_sources type: {type(research_sources)}")
|
||||
logger.info(f" - content_result keys: {list(content_result.keys()) if content_result else 'None'}")
|
||||
logger.info(f" - grounding_enabled: {grounding_enabled}")
|
||||
logger.info(f" - include_citations: {request.include_citations}")
|
||||
|
||||
# Debug: Log content_result details
|
||||
if content_result:
|
||||
logger.info(f" - content_result has citations: {'citations' in content_result}")
|
||||
logger.info(f" - content_result has sources: {'sources' in content_result}")
|
||||
if 'citations' in content_result:
|
||||
logger.info(f" - citations count: {len(content_result['citations']) if content_result['citations'] else 0}")
|
||||
if 'sources' in content_result:
|
||||
logger.info(f" - sources count: {len(content_result['sources']) if content_result['sources'] else 0}")
|
||||
|
||||
if research_sources:
|
||||
logger.info(f" - First research source: {research_sources[0] if research_sources else 'None'}")
|
||||
logger.info(f" - Research sources types: {[type(s) for s in research_sources[:3]]}")
|
||||
|
||||
# Step 3: Add citations if requested - POST METHOD
|
||||
citations = []
|
||||
source_list = None
|
||||
final_research_sources = research_sources # Default to passed research_sources
|
||||
|
||||
# Use sources and citations from content_result if available (from Gemini grounding)
|
||||
if content_result.get('citations') and content_result.get('sources'):
|
||||
logger.info(f"Using citations and sources from Gemini grounding: {len(content_result['citations'])} citations, {len(content_result['sources'])} sources")
|
||||
citations = content_result['citations']
|
||||
# Transform Gemini sources to ResearchSource format
|
||||
gemini_sources = self._transform_gemini_sources(content_result['sources'])
|
||||
source_list = self.citation_manager.generate_source_list(gemini_sources) if self.citation_manager else None
|
||||
# Use transformed sources for the response
|
||||
final_research_sources = gemini_sources
|
||||
elif request.include_citations and research_sources and self.citation_manager:
|
||||
try:
|
||||
logger.info(f"Processing citations for content length: {len(content_result['content'])}")
|
||||
citations = self.citation_manager.extract_citations(content_result['content'])
|
||||
logger.info(f"Extracted {len(citations)} citations from content")
|
||||
source_list = self.citation_manager.generate_source_list(research_sources)
|
||||
logger.info(f"Generated source list: {source_list[:200] if source_list else 'None'}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Citation processing failed: {e}")
|
||||
else:
|
||||
logger.info(f"Citation processing skipped: include_citations={request.include_citations}, research_sources={len(research_sources) if research_sources else 0}, citation_manager={self.citation_manager is not None}")
|
||||
|
||||
# Step 4: Analyze content quality
|
||||
quality_metrics = None
|
||||
if grounding_enabled and self.quality_analyzer:
|
||||
try:
|
||||
quality_handler = QualityHandler(self.quality_analyzer)
|
||||
quality_metrics = quality_handler.create_quality_metrics(
|
||||
content=content_result['content'],
|
||||
sources=final_research_sources, # Use final_research_sources
|
||||
industry=request.industry,
|
||||
grounding_enabled=grounding_enabled
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Quality analysis failed: {e}")
|
||||
|
||||
# Step 5: Build response
|
||||
post_content = PostContent(
|
||||
content=content_result['content'],
|
||||
character_count=len(content_result['content']),
|
||||
hashtags=content_result.get('hashtags', []),
|
||||
call_to_action=content_result.get('call_to_action'),
|
||||
engagement_prediction=content_result.get('engagement_prediction'),
|
||||
citations=citations,
|
||||
source_list=source_list,
|
||||
quality_metrics=quality_metrics,
|
||||
grounding_enabled=grounding_enabled,
|
||||
search_queries=content_result.get('search_queries', [])
|
||||
)
|
||||
|
||||
generation_time = (datetime.now() - start_time).total_seconds()
|
||||
|
||||
# Build grounding status
|
||||
grounding_status = {
|
||||
'status': 'success' if grounding_enabled else 'disabled',
|
||||
'sources_used': len(final_research_sources), # Use final_research_sources
|
||||
'citation_coverage': len(citations) / max(len(final_research_sources), 1) if final_research_sources else 0,
|
||||
'quality_score': quality_metrics.overall_score if quality_metrics else 0.0
|
||||
} if grounding_enabled else None
|
||||
|
||||
return LinkedInPostResponse(
|
||||
success=True,
|
||||
data=post_content,
|
||||
research_sources=final_research_sources, # Use final_research_sources
|
||||
generation_metadata={
|
||||
'model_used': 'gemini-2.0-flash-001',
|
||||
'generation_time': generation_time,
|
||||
'research_time': research_time,
|
||||
'grounding_enabled': grounding_enabled
|
||||
},
|
||||
grounding_status=grounding_status
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating LinkedIn post: {str(e)}")
|
||||
return LinkedInPostResponse(
|
||||
success=False,
|
||||
error=f"Failed to generate LinkedIn post: {str(e)}"
|
||||
)
|
||||
|
||||
async def generate_article(
|
||||
self,
|
||||
request: LinkedInArticleRequest,
|
||||
research_sources: List,
|
||||
research_time: float,
|
||||
content_result: Dict[str, Any],
|
||||
grounding_enabled: bool
|
||||
) -> LinkedInArticleResponse:
|
||||
"""Generate LinkedIn article with all processing steps."""
|
||||
try:
|
||||
start_time = datetime.now()
|
||||
|
||||
# Step 3: Add citations if requested - ARTICLE METHOD
|
||||
citations = []
|
||||
source_list = None
|
||||
final_research_sources = research_sources # Default to passed research_sources
|
||||
|
||||
# Use sources and citations from content_result if available (from Gemini grounding)
|
||||
if content_result.get('citations') and content_result.get('sources'):
|
||||
logger.info(f"Using citations and sources from Gemini grounding: {len(content_result['citations'])} citations, {len(content_result['sources'])} sources")
|
||||
citations = content_result['citations']
|
||||
# Transform Gemini sources to ResearchSource format
|
||||
gemini_sources = self._transform_gemini_sources(content_result['sources'])
|
||||
source_list = self.citation_manager.generate_source_list(gemini_sources) if self.citation_manager else None
|
||||
# Use transformed sources for the response
|
||||
final_research_sources = gemini_sources
|
||||
elif request.include_citations and research_sources and self.citation_manager:
|
||||
try:
|
||||
citations = self.citation_manager.extract_citations(content_result['content'])
|
||||
source_list = self.citation_manager.generate_source_list(research_sources)
|
||||
except Exception as e:
|
||||
logger.warning(f"Citation processing failed: {e}")
|
||||
|
||||
# Step 4: Analyze content quality
|
||||
quality_metrics = None
|
||||
if grounding_enabled and self.quality_analyzer:
|
||||
try:
|
||||
quality_handler = QualityHandler(self.quality_analyzer)
|
||||
quality_metrics = quality_handler.create_quality_metrics(
|
||||
content=content_result['content'],
|
||||
sources=final_research_sources, # Use final_research_sources
|
||||
industry=request.industry,
|
||||
grounding_enabled=grounding_enabled
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Quality analysis failed: {e}")
|
||||
|
||||
# Step 5: Build response
|
||||
article_content = ArticleContent(
|
||||
title=content_result['title'],
|
||||
content=content_result['content'],
|
||||
word_count=len(content_result['content'].split()),
|
||||
sections=content_result.get('sections', []),
|
||||
seo_metadata=content_result.get('seo_metadata'),
|
||||
image_suggestions=content_result.get('image_suggestions', []),
|
||||
reading_time=content_result.get('reading_time'),
|
||||
citations=citations,
|
||||
source_list=source_list,
|
||||
quality_metrics=quality_metrics,
|
||||
grounding_enabled=grounding_enabled,
|
||||
search_queries=content_result.get('search_queries', [])
|
||||
)
|
||||
|
||||
generation_time = (datetime.now() - start_time).total_seconds()
|
||||
|
||||
# Build grounding status
|
||||
grounding_status = {
|
||||
'status': 'success' if grounding_enabled else 'disabled',
|
||||
'sources_used': len(final_research_sources), # Use final_research_sources
|
||||
'citation_coverage': len(citations) / max(len(final_research_sources), 1) if final_research_sources else 0,
|
||||
'quality_score': quality_metrics.overall_score if quality_metrics else 0.0
|
||||
} if grounding_enabled else None
|
||||
|
||||
return LinkedInArticleResponse(
|
||||
success=True,
|
||||
data=article_content,
|
||||
research_sources=final_research_sources, # Use final_research_sources
|
||||
generation_metadata={
|
||||
'model_used': 'gemini-2.0-flash-001',
|
||||
'generation_time': generation_time,
|
||||
'research_time': research_time,
|
||||
'grounding_enabled': grounding_enabled
|
||||
},
|
||||
grounding_status=grounding_status
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating LinkedIn article: {str(e)}")
|
||||
return LinkedInArticleResponse(
|
||||
success=False,
|
||||
error=f"Failed to generate LinkedIn article: {str(e)}"
|
||||
)
|
||||
|
||||
async def generate_carousel(
|
||||
self,
|
||||
request,
|
||||
research_sources: List,
|
||||
research_time: float,
|
||||
content_result: Dict[str, Any],
|
||||
grounding_enabled: bool
|
||||
):
|
||||
"""Generate LinkedIn carousel with all processing steps."""
|
||||
try:
|
||||
start_time = datetime.now()
|
||||
|
||||
# Step 3: Add citations if requested
|
||||
citations = []
|
||||
source_list = None
|
||||
if request.include_citations and research_sources:
|
||||
# Extract citations from all slides
|
||||
all_content = " ".join([slide['content'] for slide in content_result['slides']])
|
||||
citations = self.citation_manager.extract_citations(all_content) if self.citation_manager else []
|
||||
source_list = self.citation_manager.generate_source_list(research_sources) if self.citation_manager else None
|
||||
|
||||
# Step 4: Analyze content quality
|
||||
quality_metrics = None
|
||||
if grounding_enabled and self.quality_analyzer:
|
||||
try:
|
||||
all_content = " ".join([slide['content'] for slide in content_result['slides']])
|
||||
quality_handler = QualityHandler(self.quality_analyzer)
|
||||
quality_metrics = quality_handler.create_quality_metrics(
|
||||
content=all_content,
|
||||
sources=research_sources,
|
||||
industry=request.industry,
|
||||
grounding_enabled=grounding_enabled
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Quality analysis failed: {e}")
|
||||
|
||||
# Step 5: Build response
|
||||
slides = []
|
||||
for i, slide_data in enumerate(content_result['slides']):
|
||||
slide_citations = []
|
||||
if request.include_citations and research_sources and self.citation_manager:
|
||||
slide_citations = self.citation_manager.extract_citations(slide_data['content'])
|
||||
|
||||
slides.append({
|
||||
'slide_number': i + 1,
|
||||
'title': slide_data['title'],
|
||||
'content': slide_data['content'],
|
||||
'visual_elements': slide_data.get('visual_elements', []),
|
||||
'design_notes': slide_data.get('design_notes'),
|
||||
'citations': slide_citations
|
||||
})
|
||||
|
||||
carousel_content = {
|
||||
'title': content_result['title'],
|
||||
'slides': slides,
|
||||
'cover_slide': content_result.get('cover_slide'),
|
||||
'cta_slide': content_result.get('cta_slide'),
|
||||
'design_guidelines': content_result.get('design_guidelines', {}),
|
||||
'citations': citations,
|
||||
'source_list': source_list,
|
||||
'quality_metrics': quality_metrics,
|
||||
'grounding_enabled': grounding_enabled
|
||||
}
|
||||
|
||||
generation_time = (datetime.now() - start_time).total_seconds()
|
||||
|
||||
# Build grounding status
|
||||
grounding_status = {
|
||||
'status': 'success' if grounding_enabled else 'disabled',
|
||||
'sources_used': len(research_sources),
|
||||
'citation_coverage': len(citations) / max(len(research_sources), 1) if research_sources else 0,
|
||||
'quality_score': quality_metrics.overall_score if quality_metrics else 0.0
|
||||
} if grounding_enabled else None
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': carousel_content,
|
||||
'research_sources': research_sources,
|
||||
'generation_metadata': {
|
||||
'model_used': 'gemini-2.0-flash-001',
|
||||
'generation_time': generation_time,
|
||||
'research_time': research_time,
|
||||
'grounding_enabled': grounding_enabled
|
||||
},
|
||||
'grounding_status': grounding_status
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating LinkedIn carousel: {str(e)}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': f"Failed to generate LinkedIn carousel: {str(e)}"
|
||||
}
|
||||
|
||||
async def generate_video_script(
|
||||
self,
|
||||
request,
|
||||
research_sources: List,
|
||||
research_time: float,
|
||||
content_result: Dict[str, Any],
|
||||
grounding_enabled: bool
|
||||
):
|
||||
"""Generate LinkedIn video script with all processing steps."""
|
||||
try:
|
||||
start_time = datetime.now()
|
||||
|
||||
# Step 3: Add citations if requested
|
||||
citations = []
|
||||
source_list = None
|
||||
if request.include_citations and research_sources and self.citation_manager:
|
||||
all_content = f"{content_result['hook']} {' '.join([scene['content'] for scene in content_result['main_content']])} {content_result['conclusion']}"
|
||||
citations = self.citation_manager.extract_citations(all_content)
|
||||
source_list = self.citation_manager.generate_source_list(research_sources)
|
||||
|
||||
# Step 4: Analyze content quality
|
||||
quality_metrics = None
|
||||
if grounding_enabled and self.quality_analyzer:
|
||||
try:
|
||||
all_content = f"{content_result['hook']} {' '.join([scene['content'] for scene in content_result['main_content']])} {content_result['conclusion']}"
|
||||
quality_handler = QualityHandler(self.quality_analyzer)
|
||||
quality_metrics = quality_handler.create_quality_metrics(
|
||||
content=all_content,
|
||||
sources=research_sources,
|
||||
industry=request.industry,
|
||||
grounding_enabled=grounding_enabled
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Quality analysis failed: {e}")
|
||||
|
||||
# Step 5: Build response
|
||||
video_script = {
|
||||
'hook': content_result['hook'],
|
||||
'main_content': content_result['main_content'],
|
||||
'conclusion': content_result['conclusion'],
|
||||
'captions': content_result.get('captions'),
|
||||
'thumbnail_suggestions': content_result.get('thumbnail_suggestions', []),
|
||||
'video_description': content_result.get('video_description', ''),
|
||||
'citations': citations,
|
||||
'source_list': source_list,
|
||||
'quality_metrics': quality_metrics,
|
||||
'grounding_enabled': grounding_enabled
|
||||
}
|
||||
|
||||
generation_time = (datetime.now() - start_time).total_seconds()
|
||||
|
||||
# Build grounding status
|
||||
grounding_status = {
|
||||
'status': 'success' if grounding_enabled else 'disabled',
|
||||
'sources_used': len(research_sources),
|
||||
'citation_coverage': len(citations) / max(len(research_sources), 1) if research_sources else 0,
|
||||
'quality_score': quality_metrics.overall_score if quality_metrics else 0.0
|
||||
} if grounding_enabled else None
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': video_script,
|
||||
'research_sources': research_sources,
|
||||
'generation_metadata': {
|
||||
'model_used': 'gemini-2.0-flash-001',
|
||||
'generation_time': generation_time,
|
||||
'research_time': research_time,
|
||||
'grounding_enabled': grounding_enabled
|
||||
},
|
||||
'grounding_status': grounding_status
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating LinkedIn video script: {str(e)}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': f"Failed to generate LinkedIn video script: {str(e)}"
|
||||
}
|
||||
|
||||
async def generate_comment_response(
|
||||
self,
|
||||
request,
|
||||
research_sources: List,
|
||||
research_time: float,
|
||||
content_result: Dict[str, Any],
|
||||
grounding_enabled: bool
|
||||
):
|
||||
"""Generate LinkedIn comment response with all processing steps."""
|
||||
try:
|
||||
start_time = datetime.now()
|
||||
|
||||
generation_time = (datetime.now() - start_time).total_seconds()
|
||||
|
||||
# Build grounding status
|
||||
grounding_status = {
|
||||
'status': 'success' if grounding_enabled else 'disabled',
|
||||
'sources_used': len(research_sources),
|
||||
'citation_coverage': 0, # Comments typically don't have citations
|
||||
'quality_score': 0.8 # Default quality for comments
|
||||
} if grounding_enabled else None
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'response': content_result['response'],
|
||||
'alternative_responses': content_result.get('alternative_responses', []),
|
||||
'tone_analysis': content_result.get('tone_analysis'),
|
||||
'generation_metadata': {
|
||||
'model_used': 'gemini-2.0-flash-001',
|
||||
'generation_time': generation_time,
|
||||
'research_time': research_time,
|
||||
'grounding_enabled': grounding_enabled
|
||||
},
|
||||
'grounding_status': grounding_status
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating LinkedIn comment response: {str(e)}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': f"Failed to generate LinkedIn comment response: {str(e)}"
|
||||
}
|
||||
|
||||
# Grounded content generation methods
|
||||
async def generate_grounded_post_content(self, request, research_sources: List) -> Dict[str, Any]:
|
||||
"""Generate grounded post content using the enhanced Gemini provider with native grounding."""
|
||||
try:
|
||||
if not self.gemini_grounded:
|
||||
logger.warning("Gemini Grounded Provider not available, using fallback")
|
||||
return await self.generate_fallback_post_content(request)
|
||||
|
||||
# Build the prompt for grounded generation
|
||||
prompt = self._build_post_prompt(request)
|
||||
|
||||
# Generate grounded content using native Google Search grounding
|
||||
result = await self.gemini_grounded.generate_grounded_content(
|
||||
prompt=prompt,
|
||||
content_type="linkedin_post",
|
||||
temperature=0.7,
|
||||
max_tokens=request.max_length
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating grounded post content: {str(e)}")
|
||||
# Fallback to basic generation
|
||||
return await self.generate_fallback_post_content(request)
|
||||
|
||||
async def generate_grounded_article_content(self, request, research_sources: List) -> Dict[str, Any]:
|
||||
"""Generate grounded article content using the enhanced Gemini provider with native grounding."""
|
||||
try:
|
||||
if not self.gemini_grounded:
|
||||
logger.warning("Gemini Grounded Provider not available, using fallback")
|
||||
return await self.generate_fallback_article_content(request)
|
||||
|
||||
# Build the prompt for grounded generation
|
||||
prompt = self._build_article_prompt(request)
|
||||
|
||||
# Generate grounded content using native Google Search grounding
|
||||
result = await self.gemini_grounded.generate_grounded_content(
|
||||
prompt=prompt,
|
||||
content_type="linkedin_article",
|
||||
temperature=0.7,
|
||||
max_tokens=request.word_count * 10 # Approximate character count
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating grounded article content: {str(e)}")
|
||||
# Fallback to basic generation
|
||||
return await self.generate_fallback_article_content(request)
|
||||
|
||||
async def generate_grounded_carousel_content(self, request, research_sources: List) -> Dict[str, Any]:
|
||||
"""Generate grounded carousel content using the enhanced Gemini provider with native grounding."""
|
||||
try:
|
||||
if not self.gemini_grounded:
|
||||
logger.warning("Gemini Grounded Provider not available, using fallback")
|
||||
return await self.generate_fallback_carousel_content(request)
|
||||
|
||||
# Build the prompt for grounded generation
|
||||
prompt = self._build_carousel_prompt(request)
|
||||
|
||||
# Generate grounded content using native Google Search grounding
|
||||
result = await self.gemini_grounded.generate_grounded_content(
|
||||
prompt=prompt,
|
||||
content_type="linkedin_carousel",
|
||||
temperature=0.7,
|
||||
max_tokens=2000
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating grounded carousel content: {str(e)}")
|
||||
# Fallback to basic generation
|
||||
return await self.generate_fallback_carousel_content(request)
|
||||
|
||||
async def generate_grounded_video_script_content(self, request, research_sources: List) -> Dict[str, Any]:
|
||||
"""Generate grounded video script content using the enhanced Gemini provider with native grounding."""
|
||||
try:
|
||||
if not self.gemini_grounded:
|
||||
logger.warning("Gemini Grounded Provider not available, using fallback")
|
||||
return await self.generate_fallback_video_script_content(request)
|
||||
|
||||
# Build the prompt for grounded generation
|
||||
prompt = self._build_video_script_prompt(request)
|
||||
|
||||
# Generate grounded content using native Google Search grounding
|
||||
result = await self.gemini_grounded.generate_grounded_content(
|
||||
prompt=prompt,
|
||||
content_type="linkedin_video_script",
|
||||
temperature=0.7,
|
||||
max_tokens=1500
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating grounded video script content: {str(e)}")
|
||||
# Fallback to basic generation
|
||||
return await self.generate_fallback_video_script_content(request)
|
||||
|
||||
async def generate_grounded_comment_response(self, request, research_sources: List) -> Dict[str, Any]:
|
||||
"""Generate grounded comment response using the enhanced Gemini provider with native grounding."""
|
||||
try:
|
||||
if not self.gemini_grounded:
|
||||
logger.warning("Gemini Grounded Provider not available, using fallback")
|
||||
return await self.generate_fallback_comment_response(request)
|
||||
|
||||
# Build the prompt for grounded generation
|
||||
prompt = self._build_comment_response_prompt(request)
|
||||
|
||||
# Generate grounded content using native Google Search grounding
|
||||
result = await self.gemini_grounded.generate_grounded_content(
|
||||
prompt=prompt,
|
||||
content_type="linkedin_comment_response",
|
||||
temperature=0.7,
|
||||
max_tokens=500
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating grounded comment response: {str(e)}")
|
||||
# Fallback to basic generation
|
||||
return await self.generate_fallback_comment_response(request)
|
||||
|
||||
# Fallback content generation methods
|
||||
async def generate_fallback_post_content(self, request) -> Dict[str, Any]:
|
||||
"""Generate post content using fallback provider."""
|
||||
if not self.fallback_provider:
|
||||
raise Exception("No fallback provider available")
|
||||
|
||||
return {
|
||||
'content': f"Professional LinkedIn post about {request.topic} in the {request.industry} industry.",
|
||||
'hashtags': [{'hashtag': f'#{request.industry.lower().replace(" ", "")}', 'category': 'industry', 'popularity_score': 0.8}],
|
||||
'call_to_action': "What are your thoughts on this? Share in the comments!",
|
||||
'engagement_prediction': {'estimated_likes': 50, 'estimated_comments': 5}
|
||||
}
|
||||
|
||||
async def generate_fallback_article_content(self, request) -> Dict[str, Any]:
|
||||
"""Generate article content using fallback provider."""
|
||||
if not self.fallback_provider:
|
||||
raise Exception("No fallback provider available")
|
||||
|
||||
return {
|
||||
'title': f"Comprehensive Guide to {request.topic} in {request.industry}",
|
||||
'content': f"Detailed article about {request.topic} in the {request.industry} industry.",
|
||||
'sections': [{'title': 'Introduction', 'content': 'Industry overview and context'}],
|
||||
'seo_metadata': {'keywords': [request.topic, request.industry]},
|
||||
'image_suggestions': ['Industry-related visual content'],
|
||||
'reading_time': '5 minutes'
|
||||
}
|
||||
|
||||
async def generate_fallback_carousel_content(self, request) -> Dict[str, Any]:
|
||||
"""Generate carousel content using fallback provider."""
|
||||
if not self.fallback_provider:
|
||||
raise Exception("No fallback provider available")
|
||||
|
||||
return {
|
||||
'title': f"Key Insights: {request.topic} in {request.industry}",
|
||||
'slides': [
|
||||
{'title': 'Overview', 'content': f'Introduction to {request.topic}', 'visual_elements': [], 'design_notes': 'Clean, professional design'},
|
||||
{'title': 'Key Points', 'content': f'Main insights about {request.topic}', 'visual_elements': [], 'design_notes': 'Bullet points with icons'}
|
||||
],
|
||||
'cover_slide': {'title': 'Cover', 'content': 'Professional cover slide', 'visual_elements': [], 'design_notes': 'Eye-catching design'},
|
||||
'cta_slide': {'title': 'Call to Action', 'content': 'Engage with this content', 'visual_elements': [], 'design_notes': 'Clear CTA design'},
|
||||
'design_guidelines': {'style': 'professional', 'colors': 'brand colors'}
|
||||
}
|
||||
|
||||
async def generate_fallback_video_script_content(self, request) -> Dict[str, Any]:
|
||||
"""Generate video script content using fallback provider."""
|
||||
if not self.fallback_provider:
|
||||
raise Exception("No fallback provider available")
|
||||
|
||||
return {
|
||||
'hook': f"Discover how {request.topic} is transforming the {request.industry} industry!",
|
||||
'main_content': [
|
||||
{'content': f'Introduction to {request.topic}', 'duration': '30s'},
|
||||
{'content': f'Key insights about {request.topic}', 'duration': '45s'}
|
||||
],
|
||||
'conclusion': f"Ready to explore {request.topic}? Let's dive in!",
|
||||
'captions': [f'Key point about {request.topic}'],
|
||||
'thumbnail_suggestions': ['Professional thumbnail with industry imagery'],
|
||||
'video_description': f"Video description about {request.topic}"
|
||||
}
|
||||
|
||||
async def generate_fallback_comment_response(self, request) -> Dict[str, Any]:
|
||||
"""Generate comment response using fallback provider."""
|
||||
if not self.fallback_provider:
|
||||
raise Exception("No fallback provider available")
|
||||
|
||||
return {
|
||||
'response': f"Thank you for your comment about {request.original_comment}",
|
||||
'alternative_responses': [],
|
||||
'tone_analysis': None
|
||||
}
|
||||
|
||||
# Prompt building methods
|
||||
def _build_post_prompt(self, request) -> str:
|
||||
"""Build prompt for post generation."""
|
||||
prompt = f"""
|
||||
Generate a professional LinkedIn post about {request.topic} in the {request.industry} industry.
|
||||
|
||||
Requirements:
|
||||
- Tone: {request.tone}
|
||||
- Target audience: {request.target_audience or 'Industry professionals'}
|
||||
- Maximum length: {request.max_length} characters
|
||||
- Include engaging hashtags
|
||||
- Include a call to action
|
||||
- Make it informative and shareable
|
||||
|
||||
Key points to include: {', '.join(request.key_points) if request.key_points else 'Industry insights and trends'}
|
||||
"""
|
||||
return prompt.strip()
|
||||
|
||||
def _build_article_prompt(self, request) -> str:
|
||||
"""Build prompt for article generation."""
|
||||
prompt = f"""
|
||||
Generate a comprehensive LinkedIn article about {request.topic} in the {request.industry} industry.
|
||||
|
||||
Requirements:
|
||||
- Tone: {request.tone}
|
||||
- Target audience: {request.target_audience or 'Industry professionals'}
|
||||
- Word count: {request.word_count} words
|
||||
- Include SEO optimization
|
||||
- Include image suggestions
|
||||
- Make it informative and engaging
|
||||
|
||||
Key sections to include: {', '.join(request.key_sections) if request.key_sections else 'Introduction, main content, conclusion'}
|
||||
"""
|
||||
return prompt.strip()
|
||||
|
||||
def _build_carousel_prompt(self, request) -> str:
|
||||
"""Build prompt for carousel generation."""
|
||||
prompt = f"""
|
||||
Generate a LinkedIn carousel about {request.topic} in the {request.industry} industry.
|
||||
|
||||
Requirements:
|
||||
- Tone: {request.tone}
|
||||
- Target audience: {request.target_audience or 'Industry professionals'}
|
||||
- Number of slides: {request.number_of_slides}
|
||||
- Include cover slide: {request.include_cover_slide}
|
||||
- Include CTA slide: {request.include_cta_slide}
|
||||
- Make each slide informative and visually appealing
|
||||
|
||||
Each slide should contain valuable insights and be designed for social media engagement.
|
||||
"""
|
||||
return prompt.strip()
|
||||
|
||||
def _build_video_script_prompt(self, request) -> str:
|
||||
"""Build prompt for video script generation."""
|
||||
prompt = f"""
|
||||
Generate a LinkedIn video script about {request.topic} in the {request.industry} industry.
|
||||
|
||||
Requirements:
|
||||
- Tone: {request.tone}
|
||||
- Target audience: {request.target_audience or 'Industry professionals'}
|
||||
- Duration: {request.video_duration} seconds
|
||||
- Include captions: {request.include_captions}
|
||||
- Include thumbnail suggestions: {request.include_thumbnail_suggestions}
|
||||
- Make it engaging and informative
|
||||
|
||||
Structure: Hook, main content (divided into scenes), conclusion
|
||||
"""
|
||||
return prompt.strip()
|
||||
|
||||
def _build_comment_response_prompt(self, request) -> str:
|
||||
"""Build prompt for comment response generation."""
|
||||
prompt = f"""
|
||||
Generate a LinkedIn comment response to: "{request.original_comment}"
|
||||
|
||||
Context: {request.post_context}
|
||||
Industry: {request.industry}
|
||||
Tone: {request.tone}
|
||||
Response length: {request.response_length}
|
||||
Include questions: {request.include_questions}
|
||||
|
||||
Make the response engaging, professional, and add value to the conversation.
|
||||
"""
|
||||
return prompt.strip()
|
||||
61
backend/services/linkedin/quality_handler.py
Normal file
61
backend/services/linkedin/quality_handler.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Quality Handler for LinkedIn Content Generation
|
||||
|
||||
Handles content quality analysis and metrics conversion.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
from models.linkedin_models import ContentQualityMetrics
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class QualityHandler:
|
||||
"""Handles content quality analysis and metrics conversion."""
|
||||
|
||||
def __init__(self, quality_analyzer=None):
|
||||
self.quality_analyzer = quality_analyzer
|
||||
|
||||
def create_quality_metrics(
|
||||
self,
|
||||
content: str,
|
||||
sources: list,
|
||||
industry: str,
|
||||
grounding_enabled: bool = False
|
||||
) -> Optional[ContentQualityMetrics]:
|
||||
"""
|
||||
Create ContentQualityMetrics object from quality analysis.
|
||||
|
||||
Args:
|
||||
content: Content to analyze
|
||||
sources: Research sources used
|
||||
industry: Target industry
|
||||
grounding_enabled: Whether grounding was used
|
||||
|
||||
Returns:
|
||||
ContentQualityMetrics object or None if analysis fails
|
||||
"""
|
||||
if not grounding_enabled or not self.quality_analyzer:
|
||||
return None
|
||||
|
||||
try:
|
||||
quality_analysis = self.quality_analyzer.analyze_content_quality(
|
||||
content=content,
|
||||
sources=sources,
|
||||
industry=industry
|
||||
)
|
||||
|
||||
# Convert the analysis result to ContentQualityMetrics format
|
||||
return ContentQualityMetrics(
|
||||
overall_score=quality_analysis.get('overall_score', 0.0),
|
||||
factual_accuracy=quality_analysis.get('metrics', {}).get('factual_accuracy', 0.0),
|
||||
source_verification=quality_analysis.get('metrics', {}).get('source_verification', 0.0),
|
||||
professional_tone=quality_analysis.get('metrics', {}).get('professional_tone', 0.0),
|
||||
industry_relevance=quality_analysis.get('metrics', {}).get('industry_relevance', 0.0),
|
||||
citation_coverage=quality_analysis.get('metrics', {}).get('citation_coverage', 0.0),
|
||||
content_length=quality_analysis.get('content_length', 0),
|
||||
word_count=quality_analysis.get('word_count', 0),
|
||||
analysis_timestamp=quality_analysis.get('analysis_timestamp', '')
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Quality metrics creation failed: {e}")
|
||||
return None
|
||||
76
backend/services/linkedin/research_handler.py
Normal file
76
backend/services/linkedin/research_handler.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Research Handler for LinkedIn Content Generation
|
||||
|
||||
Handles research operations and timing for content generation.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from loguru import logger
|
||||
from models.linkedin_models import ResearchSource
|
||||
|
||||
|
||||
class ResearchHandler:
|
||||
"""Handles research operations and timing for LinkedIn content."""
|
||||
|
||||
def __init__(self, linkedin_service):
|
||||
self.linkedin_service = linkedin_service
|
||||
|
||||
async def conduct_research(
|
||||
self,
|
||||
request,
|
||||
research_enabled: bool,
|
||||
search_engine: str,
|
||||
max_results: int = 10
|
||||
) -> tuple[List[ResearchSource], float]:
|
||||
"""
|
||||
Conduct research if enabled and return sources with timing.
|
||||
|
||||
Returns:
|
||||
Tuple of (research_sources, research_time)
|
||||
"""
|
||||
research_sources = []
|
||||
research_time = 0
|
||||
|
||||
if research_enabled:
|
||||
# Debug: Log the search engine value being passed
|
||||
logger.info(f"ResearchHandler: search_engine='{search_engine}' (type: {type(search_engine)})")
|
||||
|
||||
research_start = datetime.now()
|
||||
research_sources = await self.linkedin_service._conduct_research(
|
||||
topic=request.topic,
|
||||
industry=request.industry,
|
||||
search_engine=search_engine,
|
||||
max_results=max_results
|
||||
)
|
||||
research_time = (datetime.now() - research_start).total_seconds()
|
||||
logger.info(f"Research completed in {research_time:.2f}s, found {len(research_sources)} sources")
|
||||
|
||||
return research_sources, research_time
|
||||
|
||||
def determine_grounding_enabled(self, request, research_sources: List[ResearchSource]) -> bool:
|
||||
"""Determine if grounding should be enabled based on request and research results."""
|
||||
# Normalize values from possible Enum or string
|
||||
try:
|
||||
level_raw = getattr(request, 'grounding_level', 'enhanced')
|
||||
level = (getattr(level_raw, 'value', level_raw) or '').strip().lower()
|
||||
except Exception:
|
||||
level = 'enhanced'
|
||||
try:
|
||||
engine_raw = getattr(request, 'search_engine', 'google')
|
||||
engine_val = getattr(engine_raw, 'value', engine_raw)
|
||||
engine_str = str(engine_val).split('.')[-1].strip().lower()
|
||||
except Exception:
|
||||
engine_str = 'google'
|
||||
research_enabled = bool(getattr(request, 'research_enabled', True))
|
||||
|
||||
if not research_enabled or level == 'none':
|
||||
return False
|
||||
|
||||
# For Google native grounding, Gemini returns sources in the generation metadata,
|
||||
# so we should not require pre-fetched research_sources.
|
||||
if engine_str == 'google':
|
||||
return True
|
||||
|
||||
# For other engines, require that research actually returned sources
|
||||
return bool(research_sources)
|
||||
Reference in New Issue
Block a user