Skip to content

Commit 4c65c84

Browse files
committed
feat(llm): 表情标签统计口径修正、永久错误不重试、WebUI 观测空值注入
1 parent ec47a27 commit 4c65c84

9 files changed

Lines changed: 432 additions & 13 deletions

pallas/product/llm/llm_daily_stats_store.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ def _metric_weight(key: str, value: object) -> int:
8585
return int(value.get("skip") or 0) + int(value.get("defer") or 0) + int(value.get("proceed") or 0)
8686
if key == "sticker_vision":
8787
return int(value.get("requests") or 0)
88+
if key == "sticker_label":
89+
return int(value.get("submitted") or 0)
8890
if key == "totals":
8991
return sum(int(v or 0) for v in value.values() if not isinstance(v, dict))
9092
if key == "by_task":
@@ -297,6 +299,7 @@ def _prefer_complete_metric(key: str, existing: Any, incoming: Any) -> Any:
297299
"totals",
298300
"gates",
299301
"sticker_vision",
302+
"sticker_label",
300303
})
301304

302305

@@ -322,6 +325,7 @@ def merge_side_snapshot(existing: dict[str, Any] | None, snapshot: dict[str, Any
322325
"memory_rag",
323326
"gates",
324327
"sticker_vision",
328+
"sticker_label",
325329
):
326330
if key not in snapshot:
327331
continue

pallas/product/llm/model_admin.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,15 +1159,34 @@ async def fetch_llm_task_stats(
11591159
except Exception:
11601160
pass
11611161

1162+
try:
1163+
from pallas.product.llm.sticker_label_observability import fetch_sticker_label_job_stats
1164+
1165+
sticker_label = await fetch_sticker_label_job_stats()
1166+
except Exception:
1167+
sticker_label = {}
1168+
if isinstance(sticker_label, dict):
1169+
ai_body = payload.get("ai") if isinstance(payload.get("ai"), dict) else {}
1170+
day = str(ai_body.get("day_key") or bot_snap.get("day_key") or today_key())
1171+
merged_ai = {
1172+
**ai_body,
1173+
"day_key": day,
1174+
"source": ai_body.get("source") or "bot",
1175+
"sticker_label": sticker_label,
1176+
}
1177+
payload["ai"] = _normalize_ai_task_stats_snapshot(merged_ai)
1178+
try:
1179+
write_llm_daily_stats_side(day, "ai", {**payload["ai"], "reachable": True})
1180+
except Exception:
1181+
pass
1182+
11621183
try:
11631184
from pallas.product.llm.sticker_vision import fetch_sticker_vision_stats
11641185

11651186
sticker_vision = await fetch_sticker_vision_stats()
11661187
except Exception:
11671188
sticker_vision = {}
1168-
if isinstance(sticker_vision, dict) and (
1169-
int(sticker_vision.get("requests") or 0) > 0 or sticker_vision.get("recent")
1170-
):
1189+
if isinstance(sticker_vision, dict):
11711190
ai_body = payload.get("ai") if isinstance(payload.get("ai"), dict) else {}
11721191
day = str(ai_body.get("day_key") or bot_snap.get("day_key") or today_key())
11731192
merged_ai = {

pallas/product/llm/sticker_label_jobs.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,12 @@ async def enqueue_sticker_label_candidate(*, cache_key: str, content: bytes, sou
150150
prompt_version=STICKER_LABEL_PROMPT_VERSION,
151151
min_confidence=STICKER_LABEL_MIN_CONFIDENCE,
152152
):
153+
from pallas.product.llm.task_metrics import record_bot_llm_task
154+
155+
record_bot_llm_task("sticker_label", "cache_hit")
153156
return False
154157
from pallas.core.shared.utils.media_cache import bind_image_content_hash
158+
from pallas.product.llm.task_metrics import record_bot_llm_task
155159

156160
await bind_image_content_hash(cache_key, content)
157161
job = WorkJob.create(
@@ -168,10 +172,8 @@ async def enqueue_sticker_label_candidate(*, cache_key: str, content: bytes, sou
168172
"observation": {"state": "queued"},
169173
},
170174
)
171-
await build_work_job_store().requeue_terminal(job)
172-
from pallas.product.llm.task_metrics import record_bot_llm_task
173-
174-
record_bot_llm_task("sticker_label", "submit_ok")
175+
_reactivated_job, reactivated = await build_work_job_store().requeue_terminal(job)
176+
record_bot_llm_task("sticker_label", "submit_ok" if reactivated else "background_coalesced")
175177
return True
176178

177179

@@ -303,6 +305,8 @@ async def handle_sticker_label_visual(payload: dict[str, Any]) -> None:
303305
})
304306
await save_sticker_label_observation(job_id, dict(payload), observation)
305307
logger.warning("sticker label failed: job_id={} err={}", job_id, type(exc).__name__)
308+
if failure_state in {"parse_error", "no_vision"}:
309+
return
306310
raise
307311
observation.update({
308312
"state": "labeled",

pallas/product/llm/sticker_label_observability.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,51 @@
1818

1919

2020
def build_sticker_label_job_stats(records: list[dict[str, object]], *, recent_limit: int = 8) -> dict[str, object]:
21-
pending = failed = 0
21+
submitted = labeled = pending = failed = 0
22+
timeout = parse_error = no_vision = circuit_open = cache_changed = 0
2223
recent_errors: list[dict[str, object]] = []
2324
for row in sorted(records, key=lambda item: float(item.get("created_at") or 0), reverse=True):
25+
submitted += 1
2426
payload = row.get("payload") if isinstance(row.get("payload"), dict) else {}
2527
observation = payload.get("observation") if isinstance(payload.get("observation"), dict) else {}
2628
state = str(observation.get("state") or "queued")
2729
if state in {"queued", "running"}:
2830
pending += 1
31+
elif state == "labeled":
32+
labeled += 1
33+
elif state == "timeout":
34+
timeout += 1
35+
elif state == "parse_error":
36+
parse_error += 1
37+
elif state == "no_vision":
38+
no_vision += 1
39+
elif state == "circuit_open":
40+
circuit_open += 1
41+
elif state == "cache_changed":
42+
cache_changed += 1
43+
elif state == "failed":
44+
failed += 1
2945
error = str(observation.get("error") or row.get("last_error") or "").strip()
3046
if error:
31-
failed += 1
3247
if len(recent_errors) < max(1, int(recent_limit)):
3348
recent_errors.append({
3449
"job_id": str(row.get("job_id") or "")[:64],
3550
"created_at": float(row.get("created_at") or 0),
3651
"state": state,
3752
"error": error[:240],
3853
})
39-
return {"pending": pending, "failed": failed, "recent_errors": recent_errors}
54+
return {
55+
"submitted": submitted,
56+
"labeled": labeled,
57+
"pending": pending,
58+
"failed": failed,
59+
"timeout": timeout,
60+
"parse_error": parse_error,
61+
"no_vision": no_vision,
62+
"circuit_open": circuit_open,
63+
"cache_changed": cache_changed,
64+
"recent_errors": recent_errors,
65+
}
4066

4167

4268
async def fetch_sticker_label_job_stats(*, recent_limit: int = 8, aggregate_limit: int = 500) -> dict[str, object]:

pallas/product/llm/task_metrics.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"background_coalesced",
2424
"callback_ok",
2525
"callback_fail",
26+
"cache_hit",
2627
"reply_gate_skip",
2728
"reply_gate_defer",
2829
"reply_gate_proceed",

tests/features/test_llm_daily_stats_closure.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from unittest.mock import MagicMock
3+
from unittest.mock import AsyncMock, MagicMock
44

55
import pytest
66

@@ -398,3 +398,45 @@ async def test_fetch_llm_task_stats_history_fallback_ignores_images_only_live(
398398
assert payload["ai"]["tokens"]["total_tokens"] == 200
399399
assert payload["ai"]["images"]["ok_count"] == 2
400400
assert payload["ai"]["images"]["image_count"] == 2
401+
402+
403+
@pytest.mark.asyncio
404+
async def test_fetch_llm_task_stats_exposes_zero_sticker_blocks_when_empty(
405+
monkeypatch: pytest.MonkeyPatch,
406+
) -> None:
407+
bot_snapshot = {
408+
"source": "bot",
409+
"day_key": "2026-06-18",
410+
"updated_at": 1.0,
411+
"by_task": {},
412+
"totals": {},
413+
}
414+
_patch_bot_snapshots(monkeypatch, bot_snapshot)
415+
monkeypatch.setattr(
416+
"pallas.product.llm.model_admin.today_key",
417+
lambda: "2026-06-18",
418+
raising=False,
419+
)
420+
_patch_bot_tokens(monkeypatch, {})
421+
monkeypatch.setattr(
422+
"pallas.product.llm.model_admin.load_llm_daily_stats_range",
423+
lambda *, start_day, end_day: ([], start_day, end_day),
424+
)
425+
monkeypatch.setattr(
426+
"pallas.product.llm.sticker_vision.fetch_sticker_vision_stats",
427+
AsyncMock(return_value={"requests": 0, "recent": []}),
428+
)
429+
monkeypatch.setattr(
430+
"pallas.product.llm.sticker_label_observability.fetch_sticker_label_job_stats",
431+
AsyncMock(return_value={"submitted": 0, "recent_errors": []}),
432+
)
433+
434+
payload = await fetch_llm_task_stats(start="2026-06-18", end="2026-06-18")
435+
436+
sticker_vision = payload["ai"].get("sticker_vision")
437+
assert isinstance(sticker_vision, dict)
438+
assert sticker_vision.get("requests") == 0
439+
assert sticker_vision.get("recent") == []
440+
sticker_label = payload["ai"].get("sticker_label")
441+
assert isinstance(sticker_label, dict)
442+
assert sticker_label.get("submitted") == 0

tests/product/llm/test_sticker_label_jobs.py

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ async def test_enqueue_candidate_uses_hash_locator_and_redacts_durable_payload(m
6969
from pallas.product.llm.sticker_labels import content_hash_for_bytes
7070

7171
original = b"original-gif-bytes"
72-
store = SimpleNamespace(requeue_terminal=AsyncMock(side_effect=lambda job: job))
72+
store = SimpleNamespace(requeue_terminal=AsyncMock(side_effect=lambda job: (job, True)))
7373
repository = SimpleNamespace(get=AsyncMock(return_value=None))
7474
monkeypatch.setattr(sticker_label_jobs, "build_work_job_store", lambda: store)
7575
monkeypatch.setattr(sticker_label_jobs, "sticker_label_repository", lambda: repository)
@@ -140,7 +140,7 @@ async def test_low_confidence_or_old_prompt_label_requeues(monkeypatch: pytest.M
140140
)
141141
)
142142
)
143-
store = SimpleNamespace(requeue_terminal=AsyncMock(side_effect=lambda job: job))
143+
store = SimpleNamespace(requeue_terminal=AsyncMock(side_effect=lambda job: (job, True)))
144144
monkeypatch.setattr(sticker_label_jobs, "sticker_label_repository", lambda: repository)
145145
monkeypatch.setattr(sticker_label_jobs, "build_work_job_store", lambda: store)
146146
monkeypatch.setattr("pallas.core.shared.utils.media_cache.bind_image_content_hash", AsyncMock())
@@ -151,6 +151,71 @@ async def test_low_confidence_or_old_prompt_label_requeues(monkeypatch: pytest.M
151151
store.requeue_terminal.assert_awaited_once()
152152

153153

154+
@pytest.mark.asyncio
155+
async def test_enqueue_candidate_counts_cache_hit_when_label_is_sufficient(
156+
monkeypatch: pytest.MonkeyPatch,
157+
) -> None:
158+
from pallas.product.llm import sticker_label_jobs
159+
from pallas.product.llm.sticker_label_jobs import StickerLabelSource
160+
from pallas.product.llm.sticker_labels import StickerSemanticLabel, content_hash_for_bytes
161+
from pallas.product.llm.task_metrics import record_bot_llm_task
162+
163+
content = b"already-labeled"
164+
repository = SimpleNamespace(
165+
get=AsyncMock(
166+
return_value=StickerSemanticLabel(
167+
content_hash=content_hash_for_bytes(content),
168+
is_sticker=True,
169+
confidence=0.95,
170+
prompt_version=1,
171+
)
172+
)
173+
)
174+
store = SimpleNamespace(requeue_terminal=AsyncMock())
175+
metric = Mock()
176+
monkeypatch.setattr(sticker_label_jobs, "sticker_label_repository", lambda: repository)
177+
monkeypatch.setattr(sticker_label_jobs, "build_work_job_store", lambda: store)
178+
monkeypatch.setattr("pallas.product.llm.task_metrics.record_bot_llm_task", metric)
179+
180+
assert not await sticker_label_jobs.enqueue_sticker_label_candidate(
181+
cache_key="[CQ:image,file=already-labeled.image]",
182+
content=content,
183+
source=StickerLabelSource.FOLLOWUP_CANDIDATE,
184+
)
185+
store.requeue_terminal.assert_not_awaited()
186+
metric.assert_any_call("sticker_label", "cache_hit")
187+
assert not any(call.args[1] == "submit_ok" for call in metric.call_args_list)
188+
assert record_bot_llm_task is not None
189+
190+
191+
@pytest.mark.asyncio
192+
async def test_enqueue_candidate_counts_coalesced_when_job_already_pending(
193+
monkeypatch: pytest.MonkeyPatch,
194+
) -> None:
195+
from pallas.product.llm import sticker_label_jobs
196+
from pallas.product.llm.sticker_label_jobs import StickerLabelSource
197+
from pallas.product.llm.sticker_labels import content_hash_for_bytes
198+
199+
content = b"dup-content"
200+
repository = SimpleNamespace(get=AsyncMock(return_value=None))
201+
store = SimpleNamespace(requeue_terminal=AsyncMock(side_effect=lambda job: (job, False)))
202+
metric = Mock()
203+
monkeypatch.setattr(sticker_label_jobs, "sticker_label_repository", lambda: repository)
204+
monkeypatch.setattr(sticker_label_jobs, "build_work_job_store", lambda: store)
205+
monkeypatch.setattr("pallas.core.shared.utils.media_cache.bind_image_content_hash", AsyncMock())
206+
monkeypatch.setattr("pallas.product.llm.task_metrics.record_bot_llm_task", metric)
207+
208+
queued = await sticker_label_jobs.enqueue_sticker_label_candidate(
209+
cache_key="[CQ:image,file=dup.image]",
210+
content=content,
211+
source=StickerLabelSource.FOLLOWUP_CANDIDATE,
212+
)
213+
214+
assert queued is True
215+
assert not any(call.args[1] == "submit_ok" for call in metric.call_args_list)
216+
assert any(call.args == ("sticker_label", "background_coalesced") for call in metric.call_args_list)
217+
218+
154219
@pytest.mark.asyncio
155220
async def test_string_source_cannot_enqueue_a_label_job(monkeypatch: pytest.MonkeyPatch) -> None:
156221
from pallas.product.llm import sticker_label_jobs
@@ -413,3 +478,49 @@ async def test_cache_changed_is_completed_by_actual_worker(monkeypatch: pytest.M
413478

414479
assert await worker.run_once()
415480
assert await store.claim(owner="other", lease_sec=1) is None
481+
482+
483+
@pytest.mark.asyncio
484+
async def test_permanent_label_errors_do_not_retry_worker(
485+
monkeypatch: pytest.MonkeyPatch,
486+
) -> None:
487+
from pallas.core.platform.work_jobs.models import WorkJob
488+
from pallas.core.platform.work_jobs.store import MemoryWorkJobStore
489+
from pallas.core.platform.work_jobs.worker import WorkJobWorker
490+
from pallas.product.llm import sticker_label_jobs
491+
from pallas.product.llm.sticker_labels import content_hash_for_bytes
492+
493+
sticker_label_jobs.reset_sticker_label_runtime_state_for_tests()
494+
for case in ("parse_error", "no_vision"):
495+
save_observation = AsyncMock()
496+
monkeypatch.setattr(sticker_label_jobs, "save_sticker_label_observation", save_observation)
497+
monkeypatch.setattr(
498+
"pallas.core.shared.utils.media_cache.get_image_by_content_hash", AsyncMock(return_value=b"image")
499+
)
500+
501+
async def fail_vision(_content: bytes) -> None:
502+
if case == "parse_error":
503+
raise ValueError("invalid sticker label JSON")
504+
raise RuntimeError("no sticker vision endpoint")
505+
506+
monkeypatch.setattr(sticker_label_jobs, "label_sticker_with_vision", fail_vision)
507+
store = MemoryWorkJobStore()
508+
await store.enqueue(
509+
WorkJob.create(
510+
kind="sticker.label.visual",
511+
payload={"content_hash": content_hash_for_bytes(b"image"), "observation": {"state": "queued"}},
512+
idempotency_key=f"label:permanent:{case}",
513+
)
514+
)
515+
worker = WorkJobWorker(
516+
store=store, owner="worker", handlers={"sticker.label.visual": sticker_label_jobs.handle_sticker_label_visual}
517+
)
518+
519+
assert await worker.run_once()
520+
assert (await store.stats())["leased"] == 0
521+
assert (await store.stats())["dead_lettered"] == 0
522+
assert worker.metrics.snapshot()["retried_since_start"] == 0
523+
assert (await store.stats())["pending"] == 0
524+
assert await store.claim(owner="next", lease_sec=1) is None
525+
assert save_observation.await_args.args[2]["state"] in {"parse_error", "no_vision"}
526+
sticker_label_jobs.reset_sticker_label_runtime_state_for_tests()

tests/product/llm/test_sticker_label_observability.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,44 @@ async def test_requeue_stale_labels_counts_active_jobs_as_skipped(beanie_fixture
9090
)
9191

9292
assert result == {"requeued": 0, "queued": 0, "skipped": 1, "missing_cache": 0}
93+
94+
95+
def test_build_sticker_label_job_stats_counts_terminal_states_by_category() -> None:
96+
from pallas.product.llm.sticker_label_observability import build_sticker_label_job_stats
97+
98+
records = [
99+
{
100+
"job_id": f"job-{state}",
101+
"created_at": float(index),
102+
"payload": {"observation": {"state": state, "error": f"err-{state}"}},
103+
"last_error": None,
104+
}
105+
for index, state in enumerate(
106+
["labeled", "labeled", "timeout", "parse_error", "no_vision", "failed", "circuit_open", "cache_changed"]
107+
)
108+
]
109+
records.append({
110+
"job_id": "job-pending",
111+
"created_at": 100.0,
112+
"payload": {"observation": {"state": "queued"}},
113+
"last_error": None,
114+
})
115+
records.append({
116+
"job_id": "job-running",
117+
"created_at": 101.0,
118+
"payload": {"observation": {"state": "running"}},
119+
"last_error": None,
120+
})
121+
122+
stats = build_sticker_label_job_stats(records, recent_limit=3)
123+
124+
assert stats["submitted"] == 10
125+
assert stats["labeled"] == 2
126+
assert stats["timeout"] == 1
127+
assert stats["parse_error"] == 1
128+
assert stats["no_vision"] == 1
129+
assert stats["failed"] == 1
130+
assert stats["circuit_open"] == 1
131+
assert stats["cache_changed"] == 1
132+
assert stats["pending"] == 2
133+
assert len(stats["recent_errors"]) == 3

0 commit comments

Comments
 (0)