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:
@@ -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("$")
|
||||
|
||||
Reference in New Issue
Block a user