This commit is contained in:
@@ -1377,7 +1377,10 @@ def promotion_review_approve(episode_id: int):
|
||||
user = get_current_user() or {}
|
||||
username = user.get('username', 'web_admin')
|
||||
approver_hash = hash_human_approver(username)
|
||||
insight_id = promotion_gate.promote(episode_id)
|
||||
insight_id = promotion_gate.promote(
|
||||
episode_id,
|
||||
human_approver=approver_hash,
|
||||
)
|
||||
if insight_id:
|
||||
return jsonify({'ok': True, 'insight_id': insight_id, 'approver': approver_hash})
|
||||
return jsonify({'ok': False, 'error': 'promote failed'}), 500
|
||||
@@ -1390,8 +1393,16 @@ def promotion_review_approve(episode_id: int):
|
||||
def promotion_review_reject(episode_id: int):
|
||||
"""Web 介面「拒絕」按鈕"""
|
||||
try:
|
||||
from services.learning_pipeline import promotion_gate
|
||||
ok = promotion_gate.reject(episode_id, 'rejected_human', detail='web admin reject')
|
||||
from services.learning_pipeline import promotion_gate, hash_human_approver
|
||||
user = get_current_user() or {}
|
||||
username = user.get('username', 'web_admin')
|
||||
approver_hash = hash_human_approver(username)
|
||||
ok = promotion_gate.reject(
|
||||
episode_id,
|
||||
'rejected_human',
|
||||
detail='web admin reject',
|
||||
human_approver=approver_hash,
|
||||
)
|
||||
return jsonify({'ok': ok})
|
||||
except Exception as e:
|
||||
return jsonify({'ok': False, 'error': str(e)[:200]}), 500
|
||||
|
||||
@@ -8768,7 +8768,10 @@ def telegram_webhook():
|
||||
approver_hash = hash_human_approver(str(cq_from_id or ''))
|
||||
if action == 'pg_ok':
|
||||
# 人工通過 → 直接 promote(不重跑 4 stage)
|
||||
insight_id = promotion_gate.promote(episode_id)
|
||||
insight_id = promotion_gate.promote(
|
||||
episode_id,
|
||||
human_approver=approver_hash,
|
||||
)
|
||||
ack = (
|
||||
f"已晉升至 ai_insights #{insight_id}"
|
||||
if insight_id else "晉升失敗(已 log)"
|
||||
@@ -8776,7 +8779,8 @@ def telegram_webhook():
|
||||
else:
|
||||
promotion_gate.reject(
|
||||
episode_id, 'rejected_human',
|
||||
detail=f'human_approver={approver_hash}',
|
||||
detail='human rejected via Telegram',
|
||||
human_approver=approver_hash,
|
||||
)
|
||||
ack = "已駁回(rejected_human)"
|
||||
sys_log.info(
|
||||
|
||||
@@ -455,7 +455,7 @@ class PromotionGate:
|
||||
|
||||
return self._stage_4_review(episode)
|
||||
|
||||
def promote(self, episode_id: int) -> Optional[int]:
|
||||
def promote(self, episode_id: int, human_approver: Optional[str] = None) -> Optional[int]:
|
||||
"""執行晉升:寫 ai_insights + 更新 learning_episodes.{insight_id, promotion_status}。
|
||||
|
||||
Returns:
|
||||
@@ -506,10 +506,15 @@ class PromotionGate:
|
||||
UPDATE learning_episodes
|
||||
SET promotion_status = 'approved',
|
||||
insight_id = :insight_id,
|
||||
human_approver = COALESCE(:human_approver, human_approver),
|
||||
reviewed_at = NOW()
|
||||
WHERE id = :id
|
||||
"""),
|
||||
{'insight_id': insight_id, 'id': episode_id},
|
||||
{
|
||||
'insight_id': insight_id,
|
||||
'human_approver': human_approver,
|
||||
'id': episode_id,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
logger.info(
|
||||
@@ -526,12 +531,19 @@ class PromotionGate:
|
||||
logger.error("[PromotionGate] promote failed (episode_id=%s): %s", episode_id, exc)
|
||||
return None
|
||||
|
||||
def reject(self, episode_id: int, reason: str, detail: Optional[str] = None) -> bool:
|
||||
def reject(
|
||||
self,
|
||||
episode_id: int,
|
||||
reason: str,
|
||||
detail: Optional[str] = None,
|
||||
human_approver: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""拒絕晉升:標 promotion_status='rejected_*' + rejected_reason。
|
||||
|
||||
Args:
|
||||
reason: rejected_quality / rejected_hallucination / rejected_duplicate / rejected_human
|
||||
detail: 補充說明(會與 reason 拼成 rejected_reason 文本)
|
||||
human_approver: 已 hash 後的人工審核者識別;不存 PII 原文。
|
||||
"""
|
||||
valid_statuses = (
|
||||
'rejected_quality',
|
||||
@@ -555,10 +567,16 @@ class PromotionGate:
|
||||
UPDATE learning_episodes
|
||||
SET promotion_status = :status,
|
||||
rejected_reason = :rej_reason,
|
||||
human_approver = COALESCE(:human_approver, human_approver),
|
||||
reviewed_at = NOW()
|
||||
WHERE id = :id
|
||||
"""),
|
||||
{'status': reason, 'rej_reason': full_reason, 'id': episode_id},
|
||||
{
|
||||
'status': reason,
|
||||
'rej_reason': full_reason,
|
||||
'human_approver': human_approver,
|
||||
'id': episode_id,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
return True
|
||||
|
||||
@@ -303,6 +303,24 @@ class TestPromote:
|
||||
assert fake_session.execute.call_count >= 2
|
||||
fake_session.commit.assert_called_once()
|
||||
|
||||
def test_promote_records_human_approver_hash(self, monkeypatch):
|
||||
from services.learning_pipeline import PromotionGate
|
||||
|
||||
ep = _fake_episode(quality_score=0.9, weight=0.5)
|
||||
_patch_load_episode(monkeypatch, ep)
|
||||
|
||||
fake_row = MagicMock()
|
||||
fake_row.__getitem__.return_value = 555
|
||||
fake_session = MagicMock()
|
||||
fake_session.execute.return_value.fetchone.return_value = fake_row
|
||||
monkeypatch.setattr('database.manager.get_session', lambda: fake_session)
|
||||
|
||||
gate = PromotionGate()
|
||||
assert gate.promote(1, human_approver='abc123ef') == 555
|
||||
|
||||
update_params = fake_session.execute.call_args_list[1].args[1]
|
||||
assert update_params['human_approver'] == 'abc123ef'
|
||||
|
||||
def test_promote_episode_not_found_returns_none(self, monkeypatch):
|
||||
from services.learning_pipeline import PromotionGate
|
||||
_patch_load_episode(monkeypatch, None)
|
||||
@@ -337,6 +355,24 @@ class TestRejectAndMark:
|
||||
assert ok is True
|
||||
fake_session.commit.assert_called_once()
|
||||
|
||||
def test_reject_records_human_approver_hash(self, monkeypatch):
|
||||
from services.learning_pipeline import PromotionGate
|
||||
|
||||
fake_session = MagicMock()
|
||||
monkeypatch.setattr('database.manager.get_session', lambda: fake_session)
|
||||
|
||||
gate = PromotionGate()
|
||||
ok = gate.reject(
|
||||
1,
|
||||
'rejected_human',
|
||||
detail='manual reject',
|
||||
human_approver='abc123ef',
|
||||
)
|
||||
|
||||
assert ok is True
|
||||
params = fake_session.execute.call_args.args[1]
|
||||
assert params['human_approver'] == 'abc123ef'
|
||||
|
||||
def test_reject_invalid_reason_returns_false(self):
|
||||
from services.learning_pipeline import PromotionGate
|
||||
gate = PromotionGate()
|
||||
|
||||
Reference in New Issue
Block a user