Merge main into cursor/implement-usage-based-subscription-and-monitoring-0179
- Resolved merge conflicts in gemini_grounded_provider.py - Removed conflicting Python cache file - Integrated latest LinkedIn Writer features from main branch
This commit is contained in:
295
backend/api/brainstorm.py
Normal file
295
backend/api/brainstorm.py
Normal file
@@ -0,0 +1,295 @@
|
||||
"""
|
||||
Brainstorming endpoints for generating Google search prompts and running a
|
||||
single grounded search to surface topic ideas. Built for reusability across
|
||||
editors. Uses the existing Gemini provider modules.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Dict, Any, Optional
|
||||
from loguru import logger
|
||||
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response
|
||||
|
||||
try:
|
||||
from services.llm_providers.gemini_grounded_provider import GeminiGroundedProvider
|
||||
GROUNDED_AVAILABLE = True
|
||||
except Exception:
|
||||
GROUNDED_AVAILABLE = False
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/brainstorm", tags=["Brainstorming"])
|
||||
|
||||
|
||||
class PersonaPayload(BaseModel):
|
||||
persona_name: Optional[str] = None
|
||||
archetype: Optional[str] = None
|
||||
core_belief: Optional[str] = None
|
||||
tonal_range: Optional[Dict[str, Any]] = None
|
||||
linguistic_fingerprint: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class PlatformPersonaPayload(BaseModel):
|
||||
content_format_rules: Optional[Dict[str, Any]] = None
|
||||
engagement_patterns: Optional[Dict[str, Any]] = None
|
||||
content_types: Optional[Dict[str, Any]] = None
|
||||
tonal_range: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class PromptRequest(BaseModel):
|
||||
seed: str = Field(..., description="Idea seed provided by end user")
|
||||
persona: Optional[PersonaPayload] = None
|
||||
platformPersona: Optional[PlatformPersonaPayload] = None
|
||||
count: int = Field(5, ge=3, le=10, description="Number of prompts to generate (default 5)")
|
||||
|
||||
|
||||
class PromptResponse(BaseModel):
|
||||
prompts: List[str]
|
||||
|
||||
|
||||
@router.post("/prompts", response_model=PromptResponse)
|
||||
async def generate_prompts(req: PromptRequest) -> PromptResponse:
|
||||
"""Generate N high-signal Google search prompts using Gemini structured output."""
|
||||
try:
|
||||
persona_line = ""
|
||||
if req.persona:
|
||||
parts = []
|
||||
if req.persona.persona_name:
|
||||
parts.append(req.persona.persona_name)
|
||||
if req.persona.archetype:
|
||||
parts.append(f"({req.persona.archetype})")
|
||||
persona_line = " ".join(parts)
|
||||
|
||||
platform_hints = []
|
||||
if req.platformPersona and req.platformPersona.content_format_rules:
|
||||
limit = req.platformPersona.content_format_rules.get("character_limit")
|
||||
if limit:
|
||||
platform_hints.append(f"respect LinkedIn character limit {limit}")
|
||||
|
||||
sys_prompt = (
|
||||
"You are an expert LinkedIn strategist who crafts precise Google search prompts "
|
||||
"to ideate content topics. Follow Google grounding best-practices: be specific, "
|
||||
"time-bound (2024-2025), include entities, and prefer intent-rich phrasing."
|
||||
)
|
||||
|
||||
prompt = f"""
|
||||
Seed: {req.seed}
|
||||
Persona: {persona_line or 'N/A'}
|
||||
Guidelines:
|
||||
- Generate {req.count} distinct, high-signal Google search prompts.
|
||||
- Each prompt should include concrete entities (companies, tools, frameworks) when possible.
|
||||
- Prefer phrasing that yields recent, authoritative sources.
|
||||
- Avoid generic phrasing ("latest trends") unless combined with concrete qualifiers.
|
||||
- Optimize for LinkedIn thought leadership and practicality.
|
||||
{('Platform hints: ' + ', '.join(platform_hints)) if platform_hints else ''}
|
||||
|
||||
Return only the list of prompts.
|
||||
""".strip()
|
||||
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompts": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = gemini_structured_json_response(
|
||||
prompt=prompt,
|
||||
schema=schema,
|
||||
temperature=0.2,
|
||||
top_p=0.9,
|
||||
top_k=40,
|
||||
max_tokens=2048,
|
||||
system_prompt=sys_prompt,
|
||||
)
|
||||
|
||||
prompts = []
|
||||
if isinstance(result, dict) and isinstance(result.get("prompts"), list):
|
||||
prompts = [str(p).strip() for p in result["prompts"] if str(p).strip()]
|
||||
|
||||
if not prompts:
|
||||
# Minimal fallback: derive simple variations
|
||||
base = req.seed.strip()
|
||||
prompts = [
|
||||
f"Recent data-backed insights about {base}",
|
||||
f"Case studies and benchmarks on {base}",
|
||||
f"Implementation playbooks for {base}",
|
||||
f"Common pitfalls and solutions in {base}",
|
||||
f"Industry leader perspectives on {base}",
|
||||
]
|
||||
|
||||
return PromptResponse(prompts=prompts[: req.count])
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating brainstorm prompts: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
class SearchRequest(BaseModel):
|
||||
prompt: str = Field(..., description="Selected search prompt to run with grounding")
|
||||
max_tokens: int = Field(1024, ge=256, le=4096)
|
||||
|
||||
|
||||
class SearchResult(BaseModel):
|
||||
title: Optional[str] = None
|
||||
url: Optional[str] = None
|
||||
snippet: Optional[str] = None
|
||||
|
||||
|
||||
class SearchResponse(BaseModel):
|
||||
results: List[SearchResult] = []
|
||||
|
||||
|
||||
@router.post("/search", response_model=SearchResponse)
|
||||
async def run_grounded_search(req: SearchRequest) -> SearchResponse:
|
||||
"""Run a single grounded Google search via GeminiGroundedProvider and return normalized results."""
|
||||
if not GROUNDED_AVAILABLE:
|
||||
raise HTTPException(status_code=503, detail="Grounded provider not available")
|
||||
|
||||
try:
|
||||
provider = GeminiGroundedProvider()
|
||||
resp = await provider.generate_grounded_content(
|
||||
prompt=req.prompt,
|
||||
content_type="linkedin_post",
|
||||
temperature=0.3,
|
||||
max_tokens=req.max_tokens,
|
||||
)
|
||||
|
||||
items: List[SearchResult] = []
|
||||
# Normalize 'sources' if present
|
||||
for s in (resp.get("sources") or []):
|
||||
items.append(SearchResult(
|
||||
title=s.get("title") or "Source",
|
||||
url=s.get("url") or s.get("link"),
|
||||
snippet=s.get("content") or s.get("snippet")
|
||||
))
|
||||
|
||||
# Provide minimal fallback if no structured sources are returned
|
||||
if not items and resp.get("content"):
|
||||
items.append(SearchResult(title="Generated overview", url=None, snippet=resp.get("content")[:400]))
|
||||
|
||||
return SearchResponse(results=items[:10])
|
||||
except Exception as e:
|
||||
logger.error(f"Error in grounded search: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
class IdeasRequest(BaseModel):
|
||||
seed: str
|
||||
persona: Optional[PersonaPayload] = None
|
||||
platformPersona: Optional[PlatformPersonaPayload] = None
|
||||
results: List[SearchResult] = []
|
||||
count: int = 5
|
||||
|
||||
|
||||
class IdeaItem(BaseModel):
|
||||
prompt: str
|
||||
rationale: Optional[str] = None
|
||||
|
||||
|
||||
class IdeasResponse(BaseModel):
|
||||
ideas: List[IdeaItem]
|
||||
|
||||
|
||||
@router.post("/ideas", response_model=IdeasResponse)
|
||||
async def generate_brainstorm_ideas(req: IdeasRequest) -> IdeasResponse:
|
||||
"""
|
||||
Create brainstorm ideas by combining persona, seed, and Google search results.
|
||||
Uses gemini_structured_json_response for consistent output.
|
||||
"""
|
||||
try:
|
||||
# Build compact search context
|
||||
top_results = req.results[:5]
|
||||
sources_block = "\n".join(
|
||||
[
|
||||
f"- {r.title or 'Source'} | {r.url or ''} | {r.snippet or ''}"
|
||||
for r in top_results
|
||||
]
|
||||
) or "(no sources)"
|
||||
|
||||
persona_block = ""
|
||||
if req.persona:
|
||||
persona_block = (
|
||||
f"Persona: {req.persona.persona_name or ''} {('(' + req.persona.archetype + ')') if req.persona.archetype else ''}\n"
|
||||
)
|
||||
|
||||
platform_block = ""
|
||||
if req.platformPersona and req.platformPersona.content_format_rules:
|
||||
limit = req.platformPersona.content_format_rules.get("character_limit")
|
||||
platform_block = f"LinkedIn character limit: {limit}" if limit else ""
|
||||
|
||||
sys_prompt = (
|
||||
"You are an enterprise-grade LinkedIn strategist. Generate specific, non-generic "
|
||||
"brainstorm prompts suitable for LinkedIn posts or carousels. Use the provided web "
|
||||
"sources to ground ideas and the persona to align tone and style."
|
||||
)
|
||||
|
||||
prompt = f"""
|
||||
SEED IDEA: {req.seed}
|
||||
{persona_block}
|
||||
{platform_block}
|
||||
|
||||
RECENT WEB SOURCES (top {len(top_results)}):
|
||||
{sources_block}
|
||||
|
||||
TASK:
|
||||
- Propose {req.count} LinkedIn-ready brainstorm prompts tailored to the persona and grounded in the sources.
|
||||
- Each prompt should be specific and actionable for 2024–2025.
|
||||
- Prefer thought-leadership angles, contrarian takes with evidence, or practical playbooks.
|
||||
- Avoid generic phrases like "latest trends" unless qualified by entities.
|
||||
|
||||
Return JSON with an array named ideas where each item has:
|
||||
- prompt: the exact text the user can use to generate a post
|
||||
- rationale: 1–2 sentence why this works for the audience/persona
|
||||
""".strip()
|
||||
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ideas": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {"type": "string"},
|
||||
"rationale": {"type": "string"},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
result = gemini_structured_json_response(
|
||||
prompt=prompt,
|
||||
schema=schema,
|
||||
temperature=0.2,
|
||||
top_p=0.9,
|
||||
top_k=40,
|
||||
max_tokens=2048,
|
||||
system_prompt=sys_prompt,
|
||||
)
|
||||
|
||||
ideas: List[IdeaItem] = []
|
||||
if isinstance(result, dict) and isinstance(result.get("ideas"), list):
|
||||
for item in result["ideas"]:
|
||||
if isinstance(item, dict) and item.get("prompt"):
|
||||
ideas.append(IdeaItem(prompt=item["prompt"], rationale=item.get("rationale")))
|
||||
|
||||
if not ideas:
|
||||
# Fallback basic ideas from seed if model returns nothing
|
||||
ideas = [
|
||||
IdeaItem(prompt=f"Explain why {req.seed} matters now with 2 recent stats", rationale="Timely and data-backed."),
|
||||
IdeaItem(prompt=f"Common pitfalls in {req.seed} and how to avoid them", rationale="Actionable and experience-based."),
|
||||
IdeaItem(prompt=f"A step-by-step playbook to implement {req.seed}", rationale="Practical value."),
|
||||
IdeaItem(prompt=f"Case study: measurable impact of {req.seed}", rationale="Story + ROI."),
|
||||
IdeaItem(prompt=f"Contrarian take: what most get wrong about {req.seed}", rationale="Thought leadership.")
|
||||
]
|
||||
|
||||
return IdeasResponse(ideas=ideas[: req.count])
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating brainstorm ideas: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@@ -262,6 +262,75 @@ async def delete_persona(user_id: int, persona_id: int):
|
||||
logger.error(f"Error deleting persona: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to delete persona: {str(e)}")
|
||||
|
||||
async def update_platform_persona(user_id: int, platform: str, update_data: Dict[str, Any]):
|
||||
"""Update platform-specific persona fields for a user's persona.
|
||||
|
||||
This updates the underlying PlatformPersona row for the given platform.
|
||||
"""
|
||||
try:
|
||||
from services.database import get_db_session
|
||||
from models.persona_models import WritingPersona, PlatformPersona
|
||||
|
||||
session = get_db_session()
|
||||
|
||||
# Find the user's active core persona id
|
||||
core_persona = session.query(WritingPersona).filter(
|
||||
WritingPersona.user_id == user_id,
|
||||
WritingPersona.is_active == True
|
||||
).order_by(WritingPersona.created_at.desc()).first()
|
||||
|
||||
if not core_persona:
|
||||
raise HTTPException(status_code=404, detail="No active persona found for user")
|
||||
|
||||
# Find the platform persona for the requested platform
|
||||
platform_persona = session.query(PlatformPersona).filter(
|
||||
PlatformPersona.writing_persona_id == core_persona.id,
|
||||
PlatformPersona.platform_type.ilike(platform),
|
||||
PlatformPersona.is_active == True
|
||||
).first()
|
||||
|
||||
if not platform_persona:
|
||||
raise HTTPException(status_code=404, detail=f"No platform persona found for platform {platform}")
|
||||
|
||||
# Update allowed platform fields
|
||||
updatable_fields = [
|
||||
'sentence_metrics', 'lexical_features', 'rhetorical_devices', 'tonal_range',
|
||||
'stylistic_constraints', 'content_format_rules', 'engagement_patterns',
|
||||
'posting_frequency', 'content_types', 'platform_best_practices', 'algorithm_considerations'
|
||||
]
|
||||
|
||||
updated_any = False
|
||||
for field in updatable_fields:
|
||||
if field in update_data:
|
||||
setattr(platform_persona, field, update_data[field])
|
||||
updated_any = True
|
||||
|
||||
if not updated_any:
|
||||
# Nothing to update
|
||||
session.close()
|
||||
return {
|
||||
"message": "No updatable fields provided",
|
||||
"platform": platform_persona.platform_type,
|
||||
"persona_id": core_persona.id
|
||||
}
|
||||
|
||||
platform_persona.updated_at = datetime.utcnow()
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
return {
|
||||
"message": "Platform persona updated successfully",
|
||||
"platform": platform,
|
||||
"persona_id": core_persona.id,
|
||||
"updated_at": platform_persona.updated_at.isoformat()
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating platform persona: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to update platform persona: {str(e)}")
|
||||
|
||||
async def validate_persona_generation_readiness(user_id: int):
|
||||
"""Check if user has sufficient onboarding data for persona generation."""
|
||||
try:
|
||||
|
||||
@@ -32,6 +32,7 @@ from api.persona import (
|
||||
)
|
||||
|
||||
from services.persona_replication_engine import PersonaReplicationEngine
|
||||
from api.persona import update_platform_persona
|
||||
|
||||
# Create router
|
||||
router = APIRouter(prefix="/api/personas", tags=["personas"])
|
||||
@@ -204,4 +205,16 @@ async def validate_content_endpoint(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Validation failed: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Validation failed: {str(e)}")
|
||||
|
||||
@router.put("/platform/{platform}")
|
||||
async def update_platform_persona_endpoint(
|
||||
platform: str,
|
||||
update_data: Dict[str, Any],
|
||||
user_id: int = Query(1, description="User ID")
|
||||
):
|
||||
"""Update platform-specific persona fields for a user.
|
||||
|
||||
Allows editing persona fields in the UI and saving them to the database.
|
||||
"""
|
||||
return await update_platform_persona(user_id, platform, update_data)
|
||||
@@ -60,6 +60,7 @@ from api.facebook_writer.routers import facebook_router
|
||||
from routers.linkedin import router as linkedin_router
|
||||
# Import LinkedIn image generation router
|
||||
from api.linkedin_image_generation import router as linkedin_image_router
|
||||
from api.brainstorm import router as brainstorm_router
|
||||
|
||||
# Import hallucination detector router
|
||||
from api.hallucination_detector import router as hallucination_detector_router
|
||||
@@ -446,6 +447,7 @@ app.include_router(facebook_router)
|
||||
app.include_router(linkedin_router)
|
||||
# Include LinkedIn image generation router
|
||||
app.include_router(linkedin_image_router)
|
||||
app.include_router(brainstorm_router)
|
||||
|
||||
# Include hallucination detector router
|
||||
app.include_router(hallucination_detector_router)
|
||||
|
||||
Binary file not shown.
@@ -61,6 +61,8 @@ class LinkedInPostRequest(BaseModel):
|
||||
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")
|
||||
user_id: Optional[int] = Field(default=1, description="User id for persona lookup")
|
||||
persona_override: Optional[Dict[str, Any]] = Field(default=None, description="Session-only persona overrides to apply without saving")
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
@@ -96,6 +98,8 @@ class LinkedInArticleRequest(BaseModel):
|
||||
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")
|
||||
user_id: Optional[int] = Field(default=1, description="User id for persona lookup")
|
||||
persona_override: Optional[Dict[str, Any]] = Field(default=None, description="Session-only persona overrides to apply without saving")
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
|
||||
@@ -21,6 +21,7 @@ from services.linkedin.content_generator_prompts import (
|
||||
CarouselGenerator,
|
||||
VideoScriptGenerator
|
||||
)
|
||||
from services.persona_analysis_service import PersonaAnalysisService
|
||||
|
||||
|
||||
class ContentGenerator:
|
||||
@@ -340,8 +341,27 @@ class ContentGenerator:
|
||||
logger.error("Gemini Grounded Provider not available - cannot generate content without AI provider")
|
||||
raise Exception("Gemini Grounded Provider not available - cannot generate content without AI provider")
|
||||
|
||||
# Build the prompt for grounded generation using the new prompt builder
|
||||
prompt = PostPromptBuilder.build_post_prompt(request)
|
||||
# Build the prompt for grounded generation using persona if available (DB vs session override)
|
||||
persona_service = PersonaAnalysisService()
|
||||
persona_data = persona_service.get_persona_for_platform(user_id=getattr(request, 'user_id', 1), platform='linkedin') if hasattr(request, 'user_id') else None
|
||||
if getattr(request, 'persona_override', None):
|
||||
try:
|
||||
# Merge shallowly: override core and platform adaptation parts
|
||||
override = request.persona_override
|
||||
if persona_data:
|
||||
core = persona_data.get('core_persona', {})
|
||||
platform_adapt = persona_data.get('platform_adaptation', {})
|
||||
if 'core_persona' in override:
|
||||
core.update(override['core_persona'])
|
||||
if 'platform_adaptation' in override:
|
||||
platform_adapt.update(override['platform_adaptation'])
|
||||
persona_data['core_persona'] = core
|
||||
persona_data['platform_adaptation'] = platform_adapt
|
||||
else:
|
||||
persona_data = override
|
||||
except Exception:
|
||||
pass
|
||||
prompt = PostPromptBuilder.build_post_prompt(request, persona=persona_data)
|
||||
|
||||
# Generate grounded content using native Google Search grounding
|
||||
result = await self.gemini_grounded.generate_grounded_content(
|
||||
@@ -395,8 +415,26 @@ class ContentGenerator:
|
||||
logger.error("Gemini Grounded Provider not available - cannot generate content without AI provider")
|
||||
raise Exception("Gemini Grounded Provider not available - cannot generate content without AI provider")
|
||||
|
||||
# Build the prompt for grounded generation using the new prompt builder
|
||||
prompt = ArticlePromptBuilder.build_article_prompt(request)
|
||||
# Build the prompt for grounded generation using persona if available (DB vs session override)
|
||||
persona_service = PersonaAnalysisService()
|
||||
persona_data = persona_service.get_persona_for_platform(user_id=getattr(request, 'user_id', 1), platform='linkedin') if hasattr(request, 'user_id') else None
|
||||
if getattr(request, 'persona_override', None):
|
||||
try:
|
||||
override = request.persona_override
|
||||
if persona_data:
|
||||
core = persona_data.get('core_persona', {})
|
||||
platform_adapt = persona_data.get('platform_adaptation', {})
|
||||
if 'core_persona' in override:
|
||||
core.update(override['core_persona'])
|
||||
if 'platform_adaptation' in override:
|
||||
platform_adapt.update(override['platform_adaptation'])
|
||||
persona_data['core_persona'] = core
|
||||
persona_data['platform_adaptation'] = platform_adapt
|
||||
else:
|
||||
persona_data = override
|
||||
except Exception:
|
||||
pass
|
||||
prompt = ArticlePromptBuilder.build_article_prompt(request, persona=persona_data)
|
||||
|
||||
# Generate grounded content using native Google Search grounding
|
||||
result = await self.gemini_grounded.generate_grounded_content(
|
||||
|
||||
@@ -4,14 +4,14 @@ LinkedIn Article Generation Prompts
|
||||
This module contains prompt templates and builders for generating LinkedIn articles.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, Optional, Dict
|
||||
|
||||
|
||||
class ArticlePromptBuilder:
|
||||
"""Builder class for LinkedIn article generation prompts."""
|
||||
|
||||
@staticmethod
|
||||
def build_article_prompt(request: Any) -> str:
|
||||
def build_article_prompt(request: Any, persona: Optional[Dict[str, Any]] = None) -> str:
|
||||
"""
|
||||
Build prompt for article generation.
|
||||
|
||||
@@ -21,6 +21,27 @@ class ArticlePromptBuilder:
|
||||
Returns:
|
||||
Formatted prompt string for article generation
|
||||
"""
|
||||
persona_block = ""
|
||||
if persona:
|
||||
try:
|
||||
core = persona.get('core_persona', persona)
|
||||
platform_adaptation = persona.get('platform_adaptation', persona.get('platform_persona', {}))
|
||||
linguistic = core.get('linguistic_fingerprint', {})
|
||||
sentence_metrics = linguistic.get('sentence_metrics', {})
|
||||
lexical_features = linguistic.get('lexical_features', {})
|
||||
tonal_range = core.get('tonal_range', {})
|
||||
persona_block = f"""
|
||||
PERSONA CONTEXT:
|
||||
- Persona Name: {core.get('persona_name', 'N/A')}
|
||||
- Archetype: {core.get('archetype', 'N/A')}
|
||||
- Core Belief: {core.get('core_belief', 'N/A')}
|
||||
- Default Tone: {tonal_range.get('default_tone', request.tone)}
|
||||
- Avg Sentence Length: {sentence_metrics.get('average_sentence_length_words', 18)} words
|
||||
- Go-to Words: {', '.join(lexical_features.get('go_to_words', [])[:5])}
|
||||
""".rstrip()
|
||||
except Exception:
|
||||
persona_block = ""
|
||||
|
||||
prompt = f"""
|
||||
You are a senior content strategist and industry expert specializing in {request.industry}. Create a comprehensive, thought-provoking LinkedIn article that establishes authority, drives engagement, and provides genuine value to professionals in this field.
|
||||
|
||||
@@ -30,6 +51,8 @@ class ArticlePromptBuilder:
|
||||
TARGET AUDIENCE: {request.target_audience or 'Industry professionals, executives, and thought leaders'}
|
||||
WORD COUNT: {request.word_count} words
|
||||
|
||||
{persona_block}
|
||||
|
||||
CONTENT STRUCTURE:
|
||||
- Compelling headline that promises specific value
|
||||
- Engaging introduction with a hook and clear value proposition
|
||||
|
||||
@@ -4,14 +4,14 @@ LinkedIn Post Generation Prompts
|
||||
This module contains prompt templates and builders for generating LinkedIn posts.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, Optional, Dict
|
||||
|
||||
|
||||
class PostPromptBuilder:
|
||||
"""Builder class for LinkedIn post generation prompts."""
|
||||
|
||||
@staticmethod
|
||||
def build_post_prompt(request: Any) -> str:
|
||||
def build_post_prompt(request: Any, persona: Optional[Dict[str, Any]] = None) -> str:
|
||||
"""
|
||||
Build prompt for post generation.
|
||||
|
||||
@@ -21,6 +21,33 @@ class PostPromptBuilder:
|
||||
Returns:
|
||||
Formatted prompt string for post generation
|
||||
"""
|
||||
persona_block = ""
|
||||
if persona:
|
||||
try:
|
||||
# Expecting structure similar to persona_service.get_persona_for_platform output
|
||||
core = persona.get('core_persona', persona)
|
||||
platform_adaptation = persona.get('platform_adaptation', persona.get('platform_persona', {}))
|
||||
linguistic = core.get('linguistic_fingerprint', {})
|
||||
sentence_metrics = linguistic.get('sentence_metrics', {})
|
||||
lexical_features = linguistic.get('lexical_features', {})
|
||||
rhetorical_devices = linguistic.get('rhetorical_devices', {})
|
||||
tonal_range = core.get('tonal_range', {})
|
||||
|
||||
persona_block = f"""
|
||||
PERSONA CONTEXT:
|
||||
- Persona Name: {core.get('persona_name', 'N/A')}
|
||||
- Archetype: {core.get('archetype', 'N/A')}
|
||||
- Core Belief: {core.get('core_belief', 'N/A')}
|
||||
- Tone: {tonal_range.get('default_tone', request.tone)}
|
||||
- Sentence Length (avg): {sentence_metrics.get('average_sentence_length_words', 15)} words
|
||||
- Preferred Sentence Type: {sentence_metrics.get('preferred_sentence_type', 'simple and compound')}
|
||||
- Go-to Words: {', '.join(lexical_features.get('go_to_words', [])[:5])}
|
||||
- Avoid Words: {', '.join(lexical_features.get('avoid_words', [])[:5])}
|
||||
- Rhetorical Style: {rhetorical_devices.get('summary','balanced rhetorical questions and examples')}
|
||||
""".rstrip()
|
||||
except Exception:
|
||||
persona_block = ""
|
||||
|
||||
prompt = f"""
|
||||
You are an expert LinkedIn content strategist with 10+ years of experience in the {request.industry} industry. Create a highly engaging, professional LinkedIn post that drives meaningful engagement and establishes thought leadership.
|
||||
|
||||
@@ -30,6 +57,8 @@ class PostPromptBuilder:
|
||||
TARGET AUDIENCE: {request.target_audience or 'Industry professionals, decision-makers, and thought leaders'}
|
||||
MAX LENGTH: {request.max_length} characters
|
||||
|
||||
{persona_block}
|
||||
|
||||
CONTENT REQUIREMENTS:
|
||||
- Start with a compelling hook that addresses a pain point or opportunity
|
||||
- Include 2-3 specific, actionable insights or data points
|
||||
|
||||
@@ -396,9 +396,7 @@ class GeminiGroundedProvider:
|
||||
logger.error(f"First candidate structure: {dir(candidates[0]) if candidates else 'No candidates'}")
|
||||
raise ValueError("No grounding metadata found - grounding is not working properly")
|
||||
else:
|
||||
logger.error("❌ CRITICAL: No candidates found in response")
|
||||
logger.error(f"Response structure: {dir(response)}")
|
||||
raise ValueError("No candidates found in response - grounding is not working properly")
|
||||
logger.warning("⚠️ No candidates found in response. Returning content without sources.")
|
||||
|
||||
# Add content-specific processing
|
||||
if content_type == "linkedin_post":
|
||||
|
||||
Reference in New Issue
Block a user