Recovered state: integrated TrendSurferAgent, restored frontend/backend files, and cleaned up recovery scripts
This commit is contained in:
@@ -35,6 +35,7 @@ from models.blog_models import (
|
||||
)
|
||||
from services.blog_writer.blog_service import BlogWriterService
|
||||
from services.blog_writer.seo.blog_seo_recommendation_applier import BlogSEORecommendationApplier
|
||||
from services.llm_providers.main_text_generation import llm_text_gen
|
||||
from .task_manager import task_manager
|
||||
from .cache_manager import cache_manager
|
||||
from models.blog_models import MediumBlogGenerateRequest
|
||||
@@ -97,6 +98,217 @@ async def apply_seo_recommendations(
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
class BlogSectionToolRequest(BaseModel):
|
||||
section_id: str = Field(..., description="Section id in blog writer UI")
|
||||
title: Optional[str] = Field(default=None, description="Section title/heading")
|
||||
content: str = Field(..., description="Section content text")
|
||||
keywords: List[str] = Field(default_factory=list, description="Optional target keywords")
|
||||
goal: Optional[str] = Field(default=None, description="Optional optimization goal")
|
||||
|
||||
|
||||
@router.post("/section/tools/originality")
|
||||
async def section_originality_tools(
|
||||
request: BlogSectionToolRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
) -> Dict[str, Any]:
|
||||
try:
|
||||
if not current_user:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
user_id = str(current_user.get("id"))
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User ID not found in authentication token")
|
||||
|
||||
from services.intelligence.sif_integration import SIFIntegrationService
|
||||
from services.intelligence.sif_agents import ContentGuardianAgent
|
||||
|
||||
sif_service = SIFIntegrationService(user_id)
|
||||
intelligence = sif_service.intelligence_service
|
||||
|
||||
content = (request.content or "").strip()
|
||||
if len(content) < 50:
|
||||
return {
|
||||
"success": False,
|
||||
"section_id": request.section_id,
|
||||
"error": "Content too short for originality check",
|
||||
"matches": [],
|
||||
}
|
||||
|
||||
matches = await intelligence.search(content, limit=5)
|
||||
normalized_matches = []
|
||||
for m in matches or []:
|
||||
normalized_matches.append(
|
||||
{
|
||||
"id": m.get("id"),
|
||||
"score": m.get("score", 0.0),
|
||||
"excerpt": (m.get("text", "") or "")[:240],
|
||||
}
|
||||
)
|
||||
|
||||
guardian = ContentGuardianAgent(intelligence, sif_service=sif_service)
|
||||
cannibalization = await guardian.check_cannibalization(content)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"section_id": request.section_id,
|
||||
"cannibalization": cannibalization,
|
||||
"matches": normalized_matches,
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to run originality tools: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/section/tools/internal-links")
|
||||
async def section_internal_link_tools(
|
||||
request: BlogSectionToolRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
) -> Dict[str, Any]:
|
||||
try:
|
||||
if not current_user:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
user_id = str(current_user.get("id"))
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User ID not found in authentication token")
|
||||
|
||||
from services.intelligence.sif_integration import SIFIntegrationService
|
||||
from services.intelligence.sif_agents import LinkGraphAgent
|
||||
|
||||
sif_service = SIFIntegrationService(user_id)
|
||||
intelligence = sif_service.intelligence_service
|
||||
|
||||
content = (request.content or "").strip()
|
||||
suggestions = []
|
||||
if len(content) >= 50:
|
||||
link_agent = LinkGraphAgent(intelligence, sif_service=sif_service)
|
||||
suggestions = await link_agent.link_suggester(content)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"section_id": request.section_id,
|
||||
"suggestions": suggestions or [],
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to run internal link tools: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/section/tools/fact-check")
|
||||
async def section_fact_check_tools(
|
||||
request: BlogSectionToolRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
) -> Dict[str, Any]:
|
||||
try:
|
||||
if not current_user:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
user_id = str(current_user.get("id"))
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User ID not found in authentication token")
|
||||
|
||||
from services.intelligence.sif_integration import SIFIntegrationService
|
||||
from services.intelligence.sif_agents import CitationExpert
|
||||
|
||||
sif_service = SIFIntegrationService(user_id)
|
||||
intelligence = sif_service.intelligence_service
|
||||
|
||||
expert = CitationExpert(intelligence)
|
||||
content = (request.content or "").strip()
|
||||
|
||||
verification = await expert.claim_verifier(content)
|
||||
|
||||
topic = request.title or content[:120]
|
||||
citations = await expert.citation_finder(topic)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"section_id": request.section_id,
|
||||
"verification": verification,
|
||||
"citations": citations or [],
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to run fact check tools: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/section/tools/optimize")
|
||||
async def section_optimize_tools(
|
||||
request: BlogSectionToolRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
) -> Dict[str, Any]:
|
||||
try:
|
||||
if not current_user:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
user_id = str(current_user.get("id"))
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User ID not found in authentication token")
|
||||
|
||||
content = (request.content or "").strip()
|
||||
if len(content) < 50:
|
||||
return {
|
||||
"success": False,
|
||||
"section_id": request.section_id,
|
||||
"error": "Content too short for optimization",
|
||||
}
|
||||
|
||||
goal = request.goal or "readability"
|
||||
keywords_str = ", ".join(request.keywords or [])
|
||||
|
||||
system_prompt = (
|
||||
"You are an expert editor. Optimize the provided blog section while preserving meaning and tone."
|
||||
)
|
||||
prompt = (
|
||||
f"Optimization goal: {goal}\n"
|
||||
f"Target keywords (if any): {keywords_str}\n"
|
||||
f"Section title: {request.title or ''}\n\n"
|
||||
"Return a JSON object with keys:\n"
|
||||
'- optimized_content: string\n'
|
||||
'- changes_made: array of strings\n'
|
||||
"- diff_summary: string\n\n"
|
||||
f"Section content:\n{content}\n"
|
||||
)
|
||||
|
||||
json_struct = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"optimized_content": {"type": "string"},
|
||||
"changes_made": {"type": "array", "items": {"type": "string"}},
|
||||
"diff_summary": {"type": "string"},
|
||||
},
|
||||
"required": ["optimized_content", "changes_made", "diff_summary"],
|
||||
}
|
||||
|
||||
raw = llm_text_gen(prompt=prompt, system_prompt=system_prompt, json_struct=json_struct, user_id=user_id)
|
||||
data = None
|
||||
try:
|
||||
import json as _json
|
||||
|
||||
data = _json.loads(raw) if isinstance(raw, str) else raw
|
||||
except Exception:
|
||||
data = {
|
||||
"optimized_content": raw,
|
||||
"changes_made": ["Optimization applied"],
|
||||
"diff_summary": "Generated optimized version",
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"section_id": request.section_id,
|
||||
"optimized_content": data.get("optimized_content"),
|
||||
"changes_made": data.get("changes_made", []),
|
||||
"diff_summary": data.get("diff_summary"),
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to run optimize tools: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def health() -> Dict[str, Any]:
|
||||
@@ -286,7 +498,8 @@ async def generate_section(
|
||||
) -> BlogSectionResponse:
|
||||
"""Generate content for a specific section."""
|
||||
try:
|
||||
response = await service.generate_section(request)
|
||||
user_id = str(current_user.get('id', '')) if current_user else None
|
||||
response = await service.generate_section(request, user_id=user_id)
|
||||
|
||||
# Save and track text content (non-blocking)
|
||||
if response.markdown:
|
||||
@@ -981,4 +1194,4 @@ async def generate_introductions(
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate introductions: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -10,10 +10,14 @@ from pydantic import BaseModel
|
||||
from typing import Dict, Any, Optional
|
||||
from loguru import logger
|
||||
from datetime import datetime
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select
|
||||
|
||||
from services.blog_writer.seo.blog_content_seo_analyzer import BlogContentSEOAnalyzer
|
||||
from services.blog_writer.core.blog_writer_service import BlogWriterService
|
||||
from middleware.auth_middleware import get_current_user
|
||||
from services.database import get_db
|
||||
from models.seo_analysis import SEOAnalysis
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/blog-writer/seo", tags=["Blog SEO Analysis"])
|
||||
@@ -147,7 +151,8 @@ async def analyze_blog_seo(
|
||||
@router.post("/analyze-with-progress")
|
||||
async def analyze_blog_seo_with_progress(
|
||||
request: SEOAnalysisRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Analyze blog content for SEO with real-time progress updates
|
||||
@@ -158,6 +163,7 @@ async def analyze_blog_seo_with_progress(
|
||||
Args:
|
||||
request: SEOAnalysisRequest containing blog content and research data
|
||||
current_user: Authenticated user from middleware
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
Generator yielding progress updates and final results
|
||||
@@ -240,6 +246,35 @@ async def analyze_blog_seo_with_progress(
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
# Save to Database
|
||||
try:
|
||||
draft_url = f"draft:{analysis_id}"
|
||||
overall_score = analysis_results.get('overall_score', 0)
|
||||
|
||||
# Determine health status
|
||||
if overall_score >= 90:
|
||||
health_status = "excellent"
|
||||
elif overall_score >= 70:
|
||||
health_status = "good"
|
||||
elif overall_score >= 50:
|
||||
health_status = "needs_improvement"
|
||||
else:
|
||||
health_status = "poor"
|
||||
|
||||
new_analysis = SEOAnalysis(
|
||||
url=draft_url,
|
||||
overall_score=int(overall_score),
|
||||
health_status=health_status,
|
||||
timestamp=datetime.utcnow(),
|
||||
analysis_data=analysis_results
|
||||
)
|
||||
db.add(new_analysis)
|
||||
db.commit()
|
||||
logger.info(f"Saved SEO analysis results to DB for ID: {analysis_id}")
|
||||
except Exception as db_error:
|
||||
logger.error(f"Failed to save analysis to DB: {db_error}")
|
||||
# Continue without failing
|
||||
|
||||
# Final result
|
||||
yield SEOAnalysisProgress(
|
||||
analysis_id=analysis_id,
|
||||
@@ -273,27 +308,46 @@ async def analyze_blog_seo_with_progress(
|
||||
|
||||
|
||||
@router.get("/analysis/{analysis_id}")
|
||||
async def get_analysis_result(analysis_id: str):
|
||||
async def get_analysis_result(
|
||||
analysis_id: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get SEO analysis result by ID
|
||||
|
||||
Args:
|
||||
analysis_id: Unique identifier for the analysis
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
SEO analysis results
|
||||
"""
|
||||
try:
|
||||
# In a real implementation, you would store results in a database
|
||||
# For now, we'll return a placeholder
|
||||
logger.info(f"Retrieving SEO analysis result for ID: {analysis_id}")
|
||||
|
||||
return {
|
||||
"analysis_id": analysis_id,
|
||||
"status": "completed",
|
||||
"message": "Analysis results retrieved successfully"
|
||||
}
|
||||
# Look for the analysis in the database
|
||||
draft_url = f"draft:{analysis_id}"
|
||||
stmt = select(SEOAnalysis).where(SEOAnalysis.url == draft_url)
|
||||
analysis = db.execute(stmt).scalar_one_or_none()
|
||||
|
||||
if analysis and analysis.analysis_data:
|
||||
# Return stored analysis data
|
||||
return {
|
||||
"analysis_id": analysis_id,
|
||||
"status": "completed",
|
||||
"message": "Analysis results retrieved successfully",
|
||||
**analysis.analysis_data
|
||||
}
|
||||
|
||||
# If not found in DB (fallback for legacy or in-memory only)
|
||||
# For now, we return 404 to encourage DB usage, or we could return a placeholder if strictly needed.
|
||||
# But user requested DB integration, so we should rely on DB.
|
||||
|
||||
logger.warning(f"Analysis result not found in DB for ID: {analysis_id}")
|
||||
raise HTTPException(status_code=404, detail="Analysis result not found")
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Get analysis result error: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to retrieve analysis result: {str(e)}")
|
||||
|
||||
@@ -12,6 +12,8 @@ from datetime import datetime
|
||||
from typing import Any, Dict, List
|
||||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
from sqlalchemy.orm import Session
|
||||
from services.database import SessionLocal, get_session_for_user
|
||||
|
||||
from models.blog_models import (
|
||||
BlogResearchRequest,
|
||||
@@ -261,11 +263,17 @@ class TaskManager:
|
||||
if total_target > 1000:
|
||||
raise ValueError("Global target words exceed 1000; medium generation not allowed")
|
||||
|
||||
result: MediumBlogGenerateResult = await self.service.generate_medium_blog_with_progress(
|
||||
request,
|
||||
task_id,
|
||||
user_id
|
||||
)
|
||||
# Create a sync session for asset saving
|
||||
db_session = SessionLocal()
|
||||
try:
|
||||
result: MediumBlogGenerateResult = await self.service.generate_medium_blog_with_progress(
|
||||
request,
|
||||
task_id,
|
||||
user_id,
|
||||
db=db_session
|
||||
)
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
if not result or not getattr(result, "sections", None):
|
||||
raise ValueError("Empty generation result from model")
|
||||
|
||||
Reference in New Issue
Block a user