295 lines
12 KiB
Python
295 lines
12 KiB
Python
"""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,
|
||
}
|
||
|