myrmidon / python /src /server /services /blog_service.py
tek Atrust
chore(deploy): build monolithic server for Hugging Face
d5ef46f
Raw
History Blame Contribute Delete
6.5 kB
# python/src/server/services/blog_service.py
from typing import Any
from src.server.repositories.base_repository import BaseRepository
from ..config.logfire_config import get_logger
from ..utils import get_supabase_client
logger = get_logger(__name__)
class BlogService(BaseRepository):
"""Service for handling blog post operations."""
def __init__(self, supabase_client=None):
super().__init__(supabase_client or get_supabase_client())
async def list_posts(self) -> tuple[bool, dict[str, Any]]:
"""Retrieve a list of all blog posts."""
def _query():
return self.supabase_client.table("blog_posts").select("*").order("publish_date", desc=True).execute()
success, res = self.execute_query(_query, "Failed to fetch blog posts")
if success:
return True, {"posts": res.get("data", [])}
return False, res
async def get_post(self, post_id: str) -> tuple[bool, dict[str, Any]]:
"""Retrieve a single blog post by its ID."""
def _query():
return self.supabase_client.table("blog_posts").select("*").eq("id", post_id).single().execute()
success, res = self.execute_query(_query, f"Error getting post {post_id}", require_data=True)
if success:
return True, {"post": res.get("data")}
return False, res
async def create_post(self, post_data: dict[str, Any]) -> tuple[bool, dict[str, Any]]:
"""Create a new blog post with AI-generated visual asset."""
# P6: Smart Polish - Extract image and clean content
post_data = self._clean_content_images(post_data)
# Task 40.4: Automated Visual Generation (Nana Banana Integration)
if not post_data.get("cover_image"):
try:
from src.server.services.marketing_service import MarketingService
marketing_svc = MarketingService(self.supabase_client)
title = post_data.get("title", "Modern Tech")
# Request a visual asset matching the blog title style
visual = await marketing_svc.generate_visual_asset(style=f"Cyberpunk Tech Logo for {title}")
if visual.get("status") == "success":
post_data["cover_image"] = visual.get("image_url")
logger.info(f"BlogService: Automatically attached cover image (Tier: {visual.get('tier')})")
except Exception as e:
logger.warning(f"BlogService: Visual generation skipped due to error: {e}")
def _query():
return self.supabase_client.table("blog_posts").insert(post_data).execute()
success, res = self.execute_query(_query, "Error creating post")
if success:
data = res.get("data", [])
return True, {"post": data[0] if isinstance(data, list) and data else data}
return False, res
async def update_post(self, post_id: str, update_data: dict[str, Any]) -> tuple[bool, dict[str, Any]]:
"""Update an existing blog post."""
# Phase 4.6.57: AI Correction Tracking
if "content" in update_data:
# Fetch old post to compare content
old_post_success, old_post_res = await self.get_post(post_id)
if old_post_success and "post" in old_post_res:
old_content = old_post_res["post"].get("content", "")
new_content = update_data["content"]
# Calculate diff if content changed
if old_content and old_content != new_content:
import difflib
s = difflib.SequenceMatcher(None, old_content, new_content)
correction_rate = round((1.0 - s.ratio()) * 100, 2)
# Log AI_CORRECTION to archon_logs
log_data = {
"source": "blog_editor",
"level": "INFO",
"message": f"AI Content Correction: {correction_rate}% changed",
"type": "AI_CORRECTION",
"details": {
"post_id": post_id,
"correction_rate": correction_rate,
"old_length": len(old_content),
"new_length": len(new_content),
},
}
try:
self.supabase_client.table("archon_logs").insert(log_data).execute()
logger.info(f"Logged AI_CORRECTION for post {post_id} with rate {correction_rate}%")
except Exception as e:
logger.error(f"Failed to log AI_CORRECTION: {e}")
# P6: Smart Polish - Extract image and clean content
update_data = self._clean_content_images(update_data)
def _query():
return self.supabase_client.table("blog_posts").update(update_data).eq("id", post_id).execute()
success, res = self.execute_query(_query, f"Error updating post {post_id}")
if success:
data = res.get("data", [])
return True, {"post": data[0] if isinstance(data, list) and data else data}
return False, res
def _clean_content_images(self, data: dict[str, Any]) -> dict[str, Any]:
"""
P6 Implementation: Extracts image URL from Markdown syntax and strips it from content.
Moves url to metadata 'image_url' field.
"""
import re
content = data.get("content", "")
if not content:
return data
# Pattern: ![alt](url)
match = re.search(r"!\[.*?\]\((.*?)\)", content)
if match:
# Move to image_url if not already explicitly set
if not data.get("image_url"):
data["image_url"] = match.group(1)
# Remove from content
data["content"] = re.sub(r"!\[.*?\]\((.*?)\)", "", content).strip()
return data
async def delete_post(self, post_id: str) -> tuple[bool, dict[str, Any]]:
"""Delete a blog post."""
def _query():
return self.supabase_client.table("blog_posts").delete().eq("id", post_id).execute()
# execute_query with require_data=False for delete
success, res = self.execute_query(_query, f"Error deleting post {post_id}", require_data=False)
if success:
return True, {"message": "Post deleted successfully."}
return False, res