forked from retoor/devplacepy
docs: document DEVPLACE_DISABLE_SERVICES env var, news router, modal class toggle, and news service behavior
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
@@ -6,7 +7,7 @@ import httpx
|
||||
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.services.base import BaseService
|
||||
from devplacepy.utils import generate_uid
|
||||
from devplacepy.utils import generate_uid, make_combined_slug
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -24,6 +25,23 @@ def _get_setting(key: str, default: str) -> str:
|
||||
return row.get("value", default)
|
||||
|
||||
|
||||
def _get_ai_key() -> str:
|
||||
key = os.environ.get("NEWS_AI_KEY")
|
||||
if key:
|
||||
return key
|
||||
table = get_table("site_settings")
|
||||
row = table.find_one(key="news_ai_key")
|
||||
if row:
|
||||
return row.get("value", "")
|
||||
key = os.environ.get("OPENROUTER_API_KEY")
|
||||
if key:
|
||||
return key
|
||||
key = os.environ.get("OPENAI_API_KEY")
|
||||
if key:
|
||||
return key
|
||||
return ""
|
||||
|
||||
|
||||
def _extract_grade(text: str) -> int | None:
|
||||
match = re.search(r"\d+", text.strip())
|
||||
if match:
|
||||
@@ -79,17 +97,27 @@ class NewsService(BaseService):
|
||||
|
||||
news_table = get_table("news")
|
||||
images_table = get_table("news_images")
|
||||
sync_table = get_table("news_sync")
|
||||
|
||||
synced_ids = set()
|
||||
for entry in sync_table.find():
|
||||
synced_ids.add(entry["external_id"])
|
||||
|
||||
new_count = 0
|
||||
updated_count = 0
|
||||
draft_count = 0
|
||||
failed_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
for article in articles:
|
||||
external_id = article.get("guid", "")
|
||||
if not external_id:
|
||||
continue
|
||||
|
||||
if external_id in synced_ids:
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
grade = await self._grade_article(article, ai_url, ai_model)
|
||||
now = datetime.utcnow().isoformat()
|
||||
|
||||
@@ -97,23 +125,28 @@ class NewsService(BaseService):
|
||||
grade_val = 0
|
||||
auto_published = False
|
||||
failed_count += 1
|
||||
sync_status = "grading_failed"
|
||||
elif grade < threshold:
|
||||
grade_val = grade
|
||||
auto_published = False
|
||||
draft_count += 1
|
||||
sync_status = "graded"
|
||||
else:
|
||||
grade_val = grade
|
||||
auto_published = True
|
||||
sync_status = "graded"
|
||||
|
||||
is_published = "published" if auto_published else "draft"
|
||||
existing = news_table.find_one(external_id=external_id)
|
||||
|
||||
if existing:
|
||||
existing_slug = existing.get("slug", "")
|
||||
news_table.update({
|
||||
"id": existing["id"],
|
||||
"grade": grade_val,
|
||||
"status": is_published,
|
||||
"title": article.get("title", ""),
|
||||
"slug": existing_slug or make_combined_slug(article.get("title", "") or "news", existing["uid"]),
|
||||
"description": (article.get("description", "") or "")[:5000],
|
||||
"url": article.get("link", ""),
|
||||
"source_name": article.get("feed_name", ""),
|
||||
@@ -127,8 +160,10 @@ class NewsService(BaseService):
|
||||
article_uid = existing["uid"]
|
||||
else:
|
||||
article_uid = generate_uid()
|
||||
article_slug = make_combined_slug(article.get("title", "") or "news", article_uid)
|
||||
news_table.insert({
|
||||
"uid": article_uid,
|
||||
"slug": article_slug,
|
||||
"external_id": external_id,
|
||||
"title": article.get("title", ""),
|
||||
"description": (article.get("description", "") or "")[:5000],
|
||||
@@ -144,6 +179,23 @@ class NewsService(BaseService):
|
||||
})
|
||||
new_count += 1
|
||||
|
||||
existing_sync = sync_table.find_one(external_id=external_id)
|
||||
if existing_sync:
|
||||
sync_table.update({
|
||||
"id": existing_sync["id"],
|
||||
"status": sync_status,
|
||||
"synced_at": now,
|
||||
}, ["id"])
|
||||
else:
|
||||
sync_table.insert({
|
||||
"uid": generate_uid(),
|
||||
"external_id": external_id,
|
||||
"status": sync_status,
|
||||
"synced_at": now,
|
||||
})
|
||||
|
||||
synced_ids.add(external_id)
|
||||
|
||||
link = article.get("link", "")
|
||||
if link:
|
||||
fresh_images = await _get_article_images(link)
|
||||
@@ -155,7 +207,8 @@ class NewsService(BaseService):
|
||||
"alt_text": img.get("alt_text", ""),
|
||||
})
|
||||
|
||||
self.log(f"New {new_count}, updated {updated_count}, draft {draft_count}, grading failed {failed_count}")
|
||||
self.log(f"New {new_count}, updated {updated_count}, draft {draft_count}, "
|
||||
f"grading failed {failed_count}, skipped {skipped_count}")
|
||||
|
||||
async def _grade_article(self, article: dict, ai_url: str, ai_model: str) -> int | None:
|
||||
title = (article.get("title", "") or "")[:500]
|
||||
@@ -180,14 +233,25 @@ class NewsService(BaseService):
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
ai_key = _get_ai_key()
|
||||
if ai_key:
|
||||
headers["Authorization"] = f"Bearer {ai_key}"
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.post(ai_url, json=payload, headers=headers)
|
||||
resp.raise_for_status()
|
||||
if resp.status_code != 200:
|
||||
self.log(f"AI grading returned {resp.status_code}: {resp.text[:200]}")
|
||||
resp.raise_for_status()
|
||||
result = resp.json()
|
||||
text = result.get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
return _extract_grade(text)
|
||||
if not text:
|
||||
self.log(f"AI grading returned empty content for: {title[:60]}")
|
||||
return None
|
||||
grade = _extract_grade(text)
|
||||
if grade is None:
|
||||
self.log(f"AI grading returned unparseable response: {text[:100]}")
|
||||
return grade
|
||||
except Exception as e:
|
||||
self.log(f"AI grading failed: {e}")
|
||||
self.log(f"AI grading failed for '{title[:50]}': {e}")
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user