PMFlow/backend/app/services/feishu_signal_pipeline/mixin_item_processing.py

295 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Feishu signal pipeline mixin."""
from __future__ import annotations
from .shared import *
class FeishuPipelineItemProcessingMixin:
async def _process_one_item(
self,
*,
content: str,
level: str,
sender: str,
source_chat_id: str,
source_sender_open_id: Optional[str],
feishu_message_id: Optional[str],
occurred_at: datetime,
level_result: dict[str, Any],
level_confidence: float,
level_reasoning: str,
project_score: float,
cold_start_enabled: bool,
signal_auto_enter_threshold: float,
project_auto_create_threshold: float,
project_uncertain_threshold: float,
chat_context_auto_link_enabled: bool,
projects: list[dict[str, Any]],
split_from_item_id: Optional[int],
chat_type: str,
chat_context_resolution: dict[str, Any],
) -> dict[str, Any]:
item_started_at = time.perf_counter()
level_val = "project" if level == "project" else "signal"
matched_project_hint = _safe_int(chat_context_resolution.get("project_id"))
dedupe_key = self._build_queue_dedupe_key(
level=level_val,
intent=INTENT_SIGNAL_INTAKE if level_val == "signal" else INTENT_CREATE_PROJECT,
message=content,
source_chat_id=source_chat_id,
matched_project_id=matched_project_hint,
source_object=content,
)
existing_item = await self._find_active_dedupe_item(dedupe_key)
if existing_item is not None:
existing_item.occurrence_count = max(1, int(existing_item.occurrence_count or 1)) + 1
existing_item.raw_message = content[:4000]
existing_item.source_sender = (sender or "")[:120] or None
existing_item.source_sender_open_id = (source_sender_open_id or "")[:128] or None
existing_item.source_chat_id = (source_chat_id or "")[:128] or None
existing_item.source_occurred_at = occurred_at.replace(tzinfo=None)
existing_item.level_reasoning = level_reasoning[:1000]
existing_item.ai_output = json.dumps(
{
"chat_type": chat_type,
"normalized_text": content,
"level_result": level_result,
},
ensure_ascii=False,
)[:8000]
await self.db.flush()
return await self._build_existing_item_result(existing_item)
item = FeishuItem(
tenant_id=self.tenant_id,
feishu_message_id=feishu_message_id,
project_score=project_score,
signal_confidence=max(0.0, 1 - project_score),
raw_message=content[:4000],
split_from_item_id=split_from_item_id,
level=level_val,
level_confidence=level_confidence,
level_reasoning=level_reasoning[:1000],
card_type="signal" if level_val == "signal" else "new_project",
route_status=ROUTE_STATUS_CLASSIFIED,
source_chat_id=(source_chat_id or "")[:128] or None,
source_sender=(sender or "")[:120] or None,
source_sender_open_id=(source_sender_open_id or "")[:128] or None,
source_occurred_at=occurred_at.replace(tzinfo=None),
ai_output=json.dumps(
{
"chat_type": chat_type,
"normalized_text": content,
"level_result": level_result,
},
ensure_ascii=False,
)[:8000],
decision_reason=level_reasoning[:1000] or None,
safe_to_auto_execute=False,
auto_execute_state="blocked",
dedupe_key=dedupe_key,
message_class="action_required",
occurrence_count=1,
next_step_text="AI 正在校验上下文,确认后会继续推进后续动作。",
queue_visible=True,
)
self.db.add(item)
await self.db.flush()
if level_val == "project":
project_info, suggestion = await self._create_project_suggestion(
item_id=int(item.id),
content=content,
sender=sender,
fallback_confidence=level_confidence,
)
return await self._finalize_project_item(
item=item,
content=content,
feishu_message_id=feishu_message_id,
level_result=level_result,
project_info=project_info,
project_score=project_score,
signal_confidence=max(0.0, 1 - project_score),
suggestion=suggestion,
sender=sender,
cold_start_enabled=cold_start_enabled,
project_auto_create_threshold=project_auto_create_threshold,
project_uncertain_threshold=project_uncertain_threshold,
item_started_at=item_started_at,
)
simple_signal_request = self._is_simple_signal_source_request(content)
signal_info = await self._extract_signal(content, projects=projects)
matched_project_id = signal_info.get("matched_project_id")
if matched_project_id is not None:
try:
matched_project_id = int(matched_project_id)
except (TypeError, ValueError):
matched_project_id = None
confidence = _safe_float(signal_info.get("confidence"), max(0.55, 1 - project_score))
if simple_signal_request:
confidence = max(confidence, min(0.95, signal_auto_enter_threshold + 0.05))
inferred_project_id: Optional[int] = None
clarification_message: Optional[str] = None
if (chat_context_auto_link_enabled or simple_signal_request) and matched_project_id is None and source_chat_id:
inferred_project_id = _safe_int(chat_context_resolution.get("project_id"))
if inferred_project_id is not None:
matched_project_id = int(inferred_project_id)
elif bool(chat_context_resolution.get("ambiguous")):
candidate_ids = [int(x) for x in (chat_context_resolution.get("candidate_ids") or [])[:3] if _safe_int(x)]
if candidate_ids:
clarification_message = (
f"当前群最近同时涉及项目#{'、#'.join([str(pid) for pid in candidate_ids])},"
"请明确这条消息归属哪个项目。"
)
if matched_project_id is None and self._should_promote_signal_to_project_candidate(
content=content,
signal_info=signal_info,
confidence=confidence,
signal_auto_enter_threshold=signal_auto_enter_threshold,
simple_signal_request=simple_signal_request,
):
promoted_project_score = max(float(project_score or 0.0), float(project_uncertain_threshold))
item.level = "project"
item.project_score = promoted_project_score
item.signal_confidence = max(0.0, 1 - promoted_project_score)
item.card_type = "new_project_candidate"
item.matched_project_id = None
project_info, suggestion = await self._create_project_suggestion(
item_id=int(item.id),
content=content,
sender=sender,
fallback_confidence=confidence,
)
return await self._finalize_project_item(
item=item,
content=content,
feishu_message_id=feishu_message_id,
level_result=level_result,
project_info=project_info,
project_score=promoted_project_score,
signal_confidence=confidence,
suggestion=suggestion,
sender=sender,
cold_start_enabled=cold_start_enabled,
project_auto_create_threshold=project_auto_create_threshold,
project_uncertain_threshold=project_uncertain_threshold,
item_started_at=item_started_at,
extra_ai_fields={
"signal_extraction": signal_info,
"promoted_to_project_candidate": True,
"simple_signal_request": simple_signal_request,
},
)
return await self._finalize_signal_item(
item=item,
content=content,
sender=sender,
source_chat_id=source_chat_id,
source_sender_open_id=source_sender_open_id,
feishu_message_id=feishu_message_id,
level_result=level_result,
signal_info=signal_info,
matched_project_id=matched_project_id,
confidence=confidence,
project_score=project_score,
cold_start_enabled=cold_start_enabled,
signal_auto_enter_threshold=signal_auto_enter_threshold,
chat_context_resolution=chat_context_resolution,
inferred_project_id=inferred_project_id,
simple_signal_request=simple_signal_request,
clarification_message=clarification_message,
item_started_at=item_started_at,
)
async def _create_project_suggestion(
self,
*,
item_id: int,
content: str,
sender: str,
fallback_confidence: float,
) -> tuple[dict[str, Any], ProjectSuggestion]:
project_info = await self._extract_project_prefill(content)
suggestion = ProjectSuggestion(
tenant_id=self.tenant_id,
feishu_item_id=int(item_id),
suggested_name=(project_info.get("suggested_name") or content[:20] or "新项目建议")[:255],
suggested_description=(project_info.get("suggested_description") or content[:400])[:4000],
suggested_owner=(project_info.get("suggested_owner") or sender or "待分配")[:120],
suggested_timeline=(project_info.get("suggested_timeline") or "待确认")[:120],
confidence=_safe_float(project_info.get("confidence"), fallback_confidence),
reasoning=json.dumps(_normalize_reasoning(project_info.get("reasoning")), ensure_ascii=False),
status="pending",
)
self.db.add(suggestion)
await self.db.flush()
return project_info, suggestion
async def _finalize_project_item(
self,
*,
item: FeishuItem,
content: str,
feishu_message_id: Optional[str],
level_result: dict[str, Any],
project_info: dict[str, Any],
project_score: float,
signal_confidence: float,
suggestion: ProjectSuggestion,
sender: str,
cold_start_enabled: bool,
project_auto_create_threshold: float,
project_uncertain_threshold: float,
item_started_at: float,
extra_ai_fields: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
project_id = await self._route_project_item(
item=item,
suggestion=suggestion,
sender=sender,
project_score=project_score,
cold_start_enabled=cold_start_enabled,
project_auto_create_threshold=project_auto_create_threshold,
)
routing_decision, routing_reason, reasoning_bullets = self._build_project_routing_log(
project_score=project_score,
cold_start_enabled=cold_start_enabled,
project_auto_create_threshold=project_auto_create_threshold,
project_uncertain_threshold=project_uncertain_threshold,
)
ai_raw_payload: dict[str, Any] = {
"level_classification": level_result,
"project_prefill": project_info,
}
if isinstance(extra_ai_fields, dict):
ai_raw_payload.update(extra_ai_fields)
await self._write_processing_log(
feishu_message_id=feishu_message_id,
feishu_item_id=int(item.id),
raw_message=content,
ai_raw_output=json.dumps(ai_raw_payload, ensure_ascii=False),
project_score=project_score,
signal_confidence=signal_confidence,
level="project",
routing_decision=routing_decision,
routing_reason=routing_reason,
reasoning_bullets=reasoning_bullets,
processing_status="success",
error_message=None,
duration_ms=int((time.perf_counter() - item_started_at) * 1000),
)
return {
"item_id": int(item.id),
"level": "project",
"project_id": project_id,
"signal_id": None,
"route_status": item.route_status,
}