This commit is contained in:
@@ -24,6 +24,9 @@ def build_deployment_readiness_preview(
|
||||
manual_sample_review_evaluation = service.build_manual_sample_review_evaluation(
|
||||
sample_result={}
|
||||
)
|
||||
manual_sample_candidate_handoff = service.build_manual_sample_candidate_handoff(
|
||||
sample_result={}
|
||||
)
|
||||
match_review_plan = service.build_match_review_plan()
|
||||
opportunity_plan = service.build_opportunity_plan()
|
||||
opportunity_scoring_plan = service.build_opportunity_scoring_plan()
|
||||
@@ -121,6 +124,18 @@ def build_deployment_readiness_preview(
|
||||
and not manual_sample_review_evaluation["database_commit_executed"]
|
||||
and not manual_sample_review_evaluation["scheduler_attached"]
|
||||
),
|
||||
"manual_sample_candidate_handoff_post_safe": bool(
|
||||
manual_sample_candidate_handoff["mode"]
|
||||
== "manual_sample_candidate_handoff_preview"
|
||||
and manual_sample_candidate_handoff["payload_received"]
|
||||
and not manual_sample_candidate_handoff["payload_persisted"]
|
||||
and not manual_sample_candidate_handoff["candidate_handoff_persisted"]
|
||||
and not manual_sample_candidate_handoff["candidate_import_allowed"]
|
||||
and not manual_sample_candidate_handoff["external_network_executed"]
|
||||
and not manual_sample_candidate_handoff["database_write_executed"]
|
||||
and not manual_sample_candidate_handoff["database_commit_executed"]
|
||||
and not manual_sample_candidate_handoff["scheduler_attached"]
|
||||
),
|
||||
"match_review_plan_preview_safe": bool(
|
||||
match_review_plan["mode"] == "match_review_plan_preview"
|
||||
and not match_review_plan["review_queue_created"]
|
||||
@@ -363,6 +378,7 @@ def build_deployment_readiness_preview(
|
||||
"manual_sample_acceptance": manual_sample_acceptance,
|
||||
"manual_sample_review": manual_sample_review,
|
||||
"manual_sample_review_evaluation": manual_sample_review_evaluation,
|
||||
"manual_sample_candidate_handoff": manual_sample_candidate_handoff,
|
||||
"match_review_plan": match_review_plan,
|
||||
"opportunity_plan": opportunity_plan,
|
||||
"opportunity_scoring_plan": opportunity_scoring_plan,
|
||||
|
||||
@@ -345,3 +345,155 @@ def build_manual_sample_review_evaluation_preview(
|
||||
"do_not_persist_posted_review_payload",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _accepted_candidates_from_sample(sample_result, acceptance_contract, limit):
|
||||
diagnostics = sample_result.get("diagnostics") if isinstance(sample_result, dict) else {}
|
||||
diagnostics = diagnostics if isinstance(diagnostics, dict) else {}
|
||||
raw_candidates = diagnostics.get("campaign_link_candidates")
|
||||
raw_candidates = raw_candidates if isinstance(raw_candidates, list) else []
|
||||
accepted_bands = set(_thresholds(acceptance_contract)["accepted_candidate_bands"])
|
||||
normalized = [
|
||||
_normalize_candidate(item)
|
||||
for item in raw_candidates
|
||||
]
|
||||
return [
|
||||
item for item in normalized
|
||||
if item["confidence_band"] in accepted_bands
|
||||
][:limit]
|
||||
|
||||
|
||||
def build_manual_sample_candidate_handoff_preview(
|
||||
*,
|
||||
runtime_status,
|
||||
acceptance_contract,
|
||||
sample_result=None,
|
||||
payload_error=None,
|
||||
limit=20,
|
||||
):
|
||||
"""建立人工樣本候選活動 handoff;只產生 preview payload,不保存。"""
|
||||
safe_limit = max(1, min(_as_int(limit, 20), 50))
|
||||
review = build_manual_sample_review_evaluation_preview(
|
||||
runtime_status=runtime_status,
|
||||
acceptance_contract=acceptance_contract,
|
||||
sample_result=sample_result,
|
||||
payload_error=payload_error,
|
||||
)
|
||||
handoff_ready = bool(
|
||||
review["payload_valid_json_object"]
|
||||
and review["sample_result_accepted"]
|
||||
and review["ready_for_candidate_preview"]
|
||||
)
|
||||
candidates = []
|
||||
if handoff_ready:
|
||||
platform_code = str(sample_result.get("platform_code") or "")
|
||||
source_key = str(sample_result.get("source_key") or "")
|
||||
source_url = str(sample_result.get("source_url") or "")
|
||||
for index, candidate in enumerate(
|
||||
_accepted_candidates_from_sample(
|
||||
sample_result,
|
||||
acceptance_contract,
|
||||
safe_limit,
|
||||
),
|
||||
start=1,
|
||||
):
|
||||
candidates.append(
|
||||
{
|
||||
"candidate_key": (
|
||||
f"{platform_code}:{source_key}:{index}:"
|
||||
f"{candidate['confidence_band']}:{candidate['score']}"
|
||||
),
|
||||
"platform_code": platform_code,
|
||||
"source_key": source_key,
|
||||
"source_url": source_url,
|
||||
"candidate_url": candidate["url"],
|
||||
"candidate_text": candidate["text"],
|
||||
"confidence_band": candidate["confidence_band"],
|
||||
"score": candidate["score"],
|
||||
"rank_position": index,
|
||||
"review_status": "needs_operator_review",
|
||||
"write_status": "blocked_preview_only",
|
||||
"import_allowed": False,
|
||||
}
|
||||
)
|
||||
|
||||
blocked_reasons = list(review["blocked_reasons"])
|
||||
if not handoff_ready:
|
||||
blocked_reasons.append("candidate_handoff_not_ready")
|
||||
blocked_reasons.append("candidate_handoff_persist_still_blocked")
|
||||
|
||||
return {
|
||||
"mode": "manual_sample_candidate_handoff_preview",
|
||||
"review": {
|
||||
"mode": review["mode"],
|
||||
"review_result": review["review_result"],
|
||||
"sample_result_accepted": review["sample_result_accepted"],
|
||||
"ready_for_candidate_preview": review["ready_for_candidate_preview"],
|
||||
"review_findings": review["review_findings"],
|
||||
},
|
||||
"payload_received": review["payload_received"],
|
||||
"payload_valid_json_object": review["payload_valid_json_object"],
|
||||
"payload_error": review["payload_error"],
|
||||
"payload_persisted": False,
|
||||
"sample_result_persisted": False,
|
||||
"handoff_ready": handoff_ready,
|
||||
"candidate_handoff_created": bool(candidates),
|
||||
"candidate_handoff_persisted": False,
|
||||
"candidate_import_allowed": False,
|
||||
"external_network_executed": False,
|
||||
"database_connection_opened": False,
|
||||
"database_session_created": False,
|
||||
"database_write_executed": False,
|
||||
"database_commit_executed": False,
|
||||
"scheduler_attached": False,
|
||||
"writes_executed": False,
|
||||
"would_write_database": False,
|
||||
"blocked_reasons": blocked_reasons,
|
||||
"handoff_summary": {
|
||||
"candidate_count": len(candidates),
|
||||
"limit": safe_limit,
|
||||
"review_status": "needs_operator_review" if candidates else "blocked",
|
||||
"import_allowed": False,
|
||||
},
|
||||
"candidate_preview_contract": {
|
||||
"required_fields": [
|
||||
"candidate_key",
|
||||
"platform_code",
|
||||
"source_key",
|
||||
"source_url",
|
||||
"candidate_url",
|
||||
"candidate_text",
|
||||
"confidence_band",
|
||||
"score",
|
||||
"rank_position",
|
||||
"review_status",
|
||||
],
|
||||
"forbidden_actions": [
|
||||
"insert_market_campaigns",
|
||||
"insert_market_campaign_products",
|
||||
"create_crawler_run",
|
||||
"auto_import_candidates",
|
||||
],
|
||||
},
|
||||
"candidates": candidates,
|
||||
"operator_next_actions": [
|
||||
{
|
||||
"key": "review_candidate_urls",
|
||||
"label": "人工檢查候選活動 URL、文字與信心分級",
|
||||
"write_status": "blocked",
|
||||
},
|
||||
{
|
||||
"key": "promote_to_candidate_review_queue_later",
|
||||
"label": "後續需另行批准才可建立候選審核 queue",
|
||||
"write_status": "blocked",
|
||||
},
|
||||
],
|
||||
"safe_boundaries": [
|
||||
"do_not_fetch_external_pages_from_handoff_api",
|
||||
"do_not_persist_candidate_handoff_payload",
|
||||
"do_not_import_candidates_from_handoff_preview",
|
||||
"do_not_write_market_tables_from_handoff_preview",
|
||||
"do_not_attach_scheduler_from_handoff_preview",
|
||||
"do_not_touch_momo_db_lifecycle",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ from services.market_intel.manual_sample_acceptance import (
|
||||
build_manual_sample_acceptance_preview,
|
||||
)
|
||||
from services.market_intel.manual_sample_review import (
|
||||
build_manual_sample_candidate_handoff_preview,
|
||||
build_manual_sample_review_evaluation_preview,
|
||||
build_manual_sample_review_preview,
|
||||
)
|
||||
@@ -102,7 +103,7 @@ class MarketIntelRuntimeStatus:
|
||||
class MarketIntelService:
|
||||
"""市場情報入口服務,先集中 feature gate 與安全狀態。"""
|
||||
|
||||
phase = "phase_50_manual_sample_review_evaluate"
|
||||
phase = "phase_51_manual_sample_candidate_handoff"
|
||||
|
||||
def get_runtime_status(self) -> MarketIntelRuntimeStatus:
|
||||
return MarketIntelRuntimeStatus(
|
||||
@@ -476,6 +477,23 @@ class MarketIntelService:
|
||||
review["phase"] = self.phase
|
||||
return review
|
||||
|
||||
def build_manual_sample_candidate_handoff(
|
||||
self,
|
||||
sample_result=None,
|
||||
payload_error=None,
|
||||
limit=20,
|
||||
):
|
||||
"""回報人工樣本候選活動 handoff;只產生 preview,不寫 DB。"""
|
||||
handoff = build_manual_sample_candidate_handoff_preview(
|
||||
runtime_status=self.get_runtime_status(),
|
||||
acceptance_contract=self.build_manual_sample_acceptance(),
|
||||
sample_result=sample_result,
|
||||
payload_error=payload_error,
|
||||
limit=limit,
|
||||
)
|
||||
handoff["phase"] = self.phase
|
||||
return handoff
|
||||
|
||||
def build_match_review_plan(self):
|
||||
"""回報商品比對審核計畫;不建立 queue、不自動確認。"""
|
||||
schema_smoke = self.build_schema_smoke()
|
||||
|
||||
Reference in New Issue
Block a user