feat: add AI markdown reformatting and usage metering to NewsService

Add AI-powered body reformatting for valid articles in the news pipeline, converting raw text walls into clean Markdown with paragraphs, headings, and lists. Introduce `news_usage` database table and `add_news_usage`/`get_news_usage` helpers to track per-cycle gateway costs (calls, tokens, latency, USD) from response headers, reported on the admin Services page. Remove unused `.sidebar-more` CSS and apply `render_title()` to poll question/label fields.
This commit is contained in:
2026-06-18 23:46:53 +00:00
parent 6ceca3d0d4
commit f3a4667fce
13 changed files with 427 additions and 57 deletions
+126
View File
@@ -71,6 +71,33 @@ class FailingApiClient(FakeClient_news_service):
if url == API_URL:
raise httpx.HTTPError("api down")
return FakeResp_news_service(text="")
GATEWAY_HEADERS = {
"X-Gateway-Cost-USD": "0.00010000",
"X-Gateway-Model": "molodetz",
"X-Gateway-Prompt-Tokens": "500",
"X-Gateway-Completion-Tokens": "100",
"X-Gateway-Total-Tokens": "600",
"X-Gateway-Upstream-Latency-Ms": "400",
"X-Gateway-Total-Latency-Ms": "450",
}
class FakeRespHeaders(FakeResp_news_service):
def __init__(self, json_data=None, text="", status=200, headers=None):
super().__init__(json_data, text, status)
self.headers = headers or {}
class UsageClient(FakeClient_news_service):
async def post(self, url, json=None, headers=None, timeout=None):
prompt = json["messages"][0]["content"]
if "Reformat" in prompt:
body = "## Heading\n\n" + ("word " * 80)
return FakeRespHeaders(
json_data={"choices": [{"message": {"content": body}}]},
headers=GATEWAY_HEADERS,
)
grade = "9" if "HighArticle" in prompt else "3"
return FakeRespHeaders(
json_data={"choices": [{"message": {"content": grade}}]},
headers=GATEWAY_HEADERS,
)
def _settings_stub(threshold="7"):
def fake_get_setting(key, default=None):
return {
@@ -470,3 +497,102 @@ def test_apply_landing_selection_caps_at_landing_max(local_db):
1 for u in uids if news_table.find_one(uid=u)["show_on_landing"] == 1
)
assert promoted == LANDING_MAX
def test_strip_md_fence_unwraps_only_full_fence():
from devplacepy.services.news import _strip_md_fence
assert _strip_md_fence("```markdown\n# Hi\n\nBody\n```") == "# Hi\n\nBody"
assert _strip_md_fence("# Hi\n\nBody") == "# Hi\n\nBody"
embedded = "Intro\n\n```\ncode\n```"
assert _strip_md_fence(embedded) == embedded
def test_accumulate_usage_handles_headers_and_missing():
from devplacepy.services.news import _new_usage_totals, _accumulate_usage
totals = _new_usage_totals()
_accumulate_usage(totals, FakeRespHeaders(headers=GATEWAY_HEADERS))
_accumulate_usage(totals, FakeRespHeaders(headers=GATEWAY_HEADERS))
assert totals["calls"] == 2
assert totals["total_tokens"] == 1200
assert abs(totals["cost_usd"] - 0.0002) < 1e-9
_accumulate_usage(None, FakeRespHeaders(headers=GATEWAY_HEADERS))
_accumulate_usage(totals, FakeRespHeaders(headers={}))
_accumulate_usage(totals, object())
assert totals["calls"] == 2
def test_grade_article_accumulates_usage(local_db, monkeypatch):
from devplacepy.services.news import _new_usage_totals
monkeypatch.setattr(news_mod, "get_setting", _settings_stub())
totals = _new_usage_totals()
grade = run_async(
NewsService()._grade_article(
{"title": "HighArticle deep dive", "description": "d", "content": "c"},
AI_URL,
"m",
UsageClient([]),
totals,
)
)
assert grade == 9
assert totals["calls"] == 1
assert totals["total_tokens"] == 600
def test_format_article_returns_markdown_and_accumulates(local_db, monkeypatch):
from devplacepy.services.news import _new_usage_totals
monkeypatch.setattr(news_mod, "get_setting", _settings_stub())
totals = _new_usage_totals()
article = {
"title": "A solid technical headline",
"description": "d" * 120,
"content": "c" * 120,
"link": "https://example.test/post",
}
out = run_async(
NewsService()._format_article(article, AI_URL, "m", UsageClient([]), totals)
)
assert out.startswith("## Heading")
assert totals["calls"] == 1
assert totals["cost_usd"] > 0
def test_format_article_empty_body_returns_empty(local_db, monkeypatch):
monkeypatch.setattr(news_mod, "get_setting", _settings_stub())
out = run_async(
NewsService()._format_article(
{"title": "t", "description": "", "content": ""},
AI_URL,
"m",
UsageClient([]),
)
)
assert out == ""
def test_collect_metrics_reports_ai_usage(local_db):
from devplacepy.database import add_news_usage
get_table("news_usage").delete()
add_news_usage(
{
"calls": 2,
"prompt_tokens": 100,
"completion_tokens": 20,
"total_tokens": 120,
"cost_usd": 0.0002,
"upstream_latency_ms": 300.0,
"total_latency_ms": 350.0,
}
)
metrics = NewsService().collect_metrics()
labels = {stat["label"]: stat["value"] for stat in metrics["stats"]}
assert labels["AI calls"] == 2
assert labels["Total tokens"] == 120
assert labels["Total cost"].startswith("$")
assert labels["Avg cost/call"].startswith("$")