PMFlow/backend/app/services/signal_capture_service/mixin_pipeline_cache.py

261 lines
11 KiB
Python

from __future__ import annotations
from .shared import *
class SignalCapturePipelineCacheMixin:
async def _sync_feishu_recent_cache(
self,
*,
tenant_id: int,
group_chat_id: str,
sender: str,
sender_open_id: Optional[str],
message: str,
pipeline_result: Dict[str, Any],
) -> None:
if not isinstance(pipeline_result, dict):
return
conversation_key = _build_feishu_rag_conversation_key(
source_chat_id=group_chat_id,
source_sender_open_id=sender_open_id,
sender=sender,
)
external_session_id = AdaptiveRAGService._external_session_id_from_key(conversation_key)
if external_session_id is None:
return
answer_text = str(pipeline_result.get("direct_reply_text") or "").strip()
if not answer_text:
answer_text = self._build_recent_cache_ack_text(pipeline_result)
if not answer_text:
return
recent_messages = await context_cache_service.get_session_messages(
tenant_id=int(tenant_id),
session_id=int(external_session_id),
)
recent_turns = context_cache_service.messages_to_turns(recent_messages)
if recent_turns:
latest_turn = recent_turns[-1]
latest_question = str(latest_turn.get("question") or "").strip()
latest_answer = str(latest_turn.get("answer") or "").strip()
if latest_question == str(message or "").strip() and latest_answer == answer_text:
return
await context_cache_service.append_session_messages(
tenant_id=int(tenant_id),
session_id=int(external_session_id),
question=str(message or "")[:3000],
answer=answer_text[:4000],
)
@staticmethod
def _build_recent_cache_ack_text(pipeline_result: Dict[str, Any]) -> str:
status = str(pipeline_result.get("status") or "").strip().lower()
if status == "captured":
return "已收到并记录该需求,已进入信号处理队列。"
if status == "ignored":
return "已收到该消息,本次未进入需求处理队列。"
if status == "error":
return str(pipeline_result.get("direct_reply_text") or "系统处理中出现异常,请稍后重试。").strip()
return "已收到该消息。"
async def _apply_pipeline_post_actions(
self,
*,
trace_id: str,
tenant_id: int,
cleaned_message: str,
proposer_name: str,
captured_at: datetime,
pipeline_result: Dict[str, Any],
) -> Dict[str, Any]:
if not isinstance(pipeline_result, dict):
return pipeline_result
if str(pipeline_result.get("status") or "").lower() != "captured":
return pipeline_result
existing_tasks = pipeline_result.get("auto_tasks")
if isinstance(existing_tasks, list) and existing_tasks:
return pipeline_result
linked_project_ids = pipeline_result.get("linked_project_ids")
if not isinstance(linked_project_ids, list) or not linked_project_ids:
return pipeline_result
signal_id = self._to_int(pipeline_result.get("signal_id"))
project_id = self._to_int(linked_project_ids[0])
if signal_id is None or project_id is None:
return pipeline_result
signal = (
await self.db.execute(
select(Signal).where(
and_(
Signal.id == signal_id,
Signal.tenant_id == tenant_id,
)
)
)
).scalar_one_or_none()
if signal is None:
return pipeline_result
if self._is_question_like(cleaned_message):
return pipeline_result
source_orchestration_result: Optional[Dict[str, Any]] = None
if SignalSourceOrchestrationService.looks_like_signal_source_request(cleaned_message):
try:
source_orchestration_result = await SignalSourceOrchestrationService(self.db).orchestrate_from_signal(
tenant_id=tenant_id,
project_id=project_id,
signal_id=signal_id,
initiating_text=cleaned_message,
requested_via="feishu",
operator_user_id=None,
dry_run=False,
)
except Exception as exc:
self._emit_ai_think(
"pipeline_post_source_onboard_failed",
trace_id=trace_id,
signal_id=signal_id,
project_id=project_id,
error=str(exc),
)
else:
orchestration_status = str(source_orchestration_result.get("status") or "").lower()
if orchestration_status in {"completed", "partial", "planned", "pending_manual"}:
auto_tasks = self._build_openclaw_auto_tasks(
source_orchestration_result.get("execution")
if isinstance(source_orchestration_result.get("execution"), dict)
else source_orchestration_result.get("openclaw"),
)
try:
strategic_payload: Dict[str, Any] = {}
raw_strategic = signal.strategic_analysis
if isinstance(raw_strategic, dict):
strategic_payload = dict(raw_strategic)
elif isinstance(raw_strategic, str) and raw_strategic.strip():
parsed = json.loads(raw_strategic)
if isinstance(parsed, dict):
strategic_payload = parsed
strategic_payload["signal_source_orchestration"] = source_orchestration_result
strategic_payload["ai_execution"] = source_orchestration_result
signal.strategic_analysis = json.dumps(strategic_payload, ensure_ascii=False)
signal.tasks_generated = json.dumps(auto_tasks, ensure_ascii=False)
signal.processed = orchestration_status in {"completed", "partial"}
await self.db.commit()
except Exception as exc:
self._emit_ai_think(
"pipeline_post_update_signal_failed",
trace_id=trace_id,
signal_id=signal_id,
project_id=project_id,
error=str(exc),
)
pipeline_result["auto_tasks"] = auto_tasks
pipeline_result["ai_execution"] = source_orchestration_result
pipeline_result["signal_source_orchestration"] = source_orchestration_result
return pipeline_result
signal_type = str(signal.signal_type or "").strip()
recommended_action = str(signal.recommended_action or "").strip()
should_orchestrate = (
recommended_action in {"create_charter", "trigger_alert"}
or signal_type == "新需求"
)
if not should_orchestrate:
return pipeline_result
classification_payload: Dict[str, Any] = {
"classification": str(pipeline_result.get("classification") or "signal"),
"impact_score": float(signal.impact_score or 0.0),
"priority": str(signal.priority or "medium"),
"confidence": float(signal.confidence or 0.0),
"reason": str(signal.summary or "")[:300],
"matched_project_ids": [project_id],
"matched_project_titles": [],
}
try:
auto_tasks, ai_execution = await self._auto_orchestrate_project_tasks(
trace_id=f"{trace_id}-pipeline",
tenant_id=tenant_id,
project_id=project_id,
signal_id=signal_id,
cleaned_message=cleaned_message,
classification=classification_payload,
proposer_name=proposer_name,
captured_at=captured_at,
)
except Exception as exc:
self._emit_ai_think(
"pipeline_post_orchestrate_failed",
trace_id=trace_id,
signal_id=signal_id,
project_id=project_id,
error=str(exc),
)
return pipeline_result
try:
strategic_payload: Dict[str, Any] = {}
raw_strategic = signal.strategic_analysis
if isinstance(raw_strategic, dict):
strategic_payload = dict(raw_strategic)
elif isinstance(raw_strategic, str) and raw_strategic.strip():
parsed = json.loads(raw_strategic)
if isinstance(parsed, dict):
strategic_payload = parsed
strategic_payload["ai_execution"] = ai_execution
signal.strategic_analysis = json.dumps(strategic_payload, ensure_ascii=False)
signal.tasks_generated = json.dumps(auto_tasks, ensure_ascii=False)
signal.processed = bool(signal.processed or auto_tasks)
await self.db.commit()
except Exception as exc:
self._emit_ai_think(
"pipeline_post_update_signal_failed",
trace_id=trace_id,
signal_id=signal_id,
project_id=project_id,
error=str(exc),
)
pipeline_result["auto_tasks"] = auto_tasks
pipeline_result["ai_execution"] = ai_execution
return pipeline_result
@staticmethod
def _build_openclaw_auto_tasks(openclaw_payload: Any) -> List[Dict[str, Any]]:
if not isinstance(openclaw_payload, dict):
return []
result = openclaw_payload.get("result") if isinstance(openclaw_payload.get("result"), dict) else openclaw_payload
execution = result.get("execution") if isinstance(result, dict) and isinstance(result.get("execution"), dict) else {}
rows = execution.get("results") if isinstance(execution.get("results"), list) else []
items: List[Dict[str, Any]] = []
for row in rows:
if not isinstance(row, dict):
continue
operation_code = str(row.get("operation_code") or "").strip()
payload = row.get("payload") if isinstance(row.get("payload"), dict) else {}
status = str(row.get("status") or "").strip()
if operation_code not in {"task.create", "task.assign", "task.prioritize", "task.status_transition"}:
continue
if status not in {"applied", "planned", "skipped"}:
continue
items.append(
{
"operation_code": operation_code,
"status": status,
"entity_id": row.get("entity_id"),
"task_id": payload.get("task_id"),
"title": payload.get("title"),
"summary": str(row.get("message") or "").strip(),
}
)
return items