PMFlow/backend/app/services/event_workflow.py

616 lines
18 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.

"""
Event-Driven Workflow Engine - 事件驱动工作流引擎
核心功能:
1. 替代固定 Sprint 迭代,实现实时事件驱动迭代
2. 市场变化可在10分钟内反映到项目优先级
3. 自动触发任务重排、依赖更新
4. 推送实时通知给相关角色
工作流事件:
- 高影响信号到达 → 自动重新评估项目优先级
- 阻塞检测 → 触发替代方案生成
- 认知冲突 → 触发对齐流程
- 技术栈变更 → 自动更新任务依赖
- 竞品动态 → 触发战略调整
"""
import asyncio
import json
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Callable
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy import text, update
from ..models import (
User, Project, Task, Signal, CognitiveConflict,
TruthNode, ProjectSignalLink, UserRole
)
from .llm import LLMService
from ..events import EventType, Event, get_event_bus
from ..utils.timezone import now_cn_aware
class WorkflowEventType:
"""工作流事件类型"""
HIGH_IMPACT_SIGNAL = "high_impact_signal"
BLOCKER_DETECTED = "blocker_detected"
COGNITIVE_CONFLICT = "cognitive_conflict"
TECH_STACK_CHANGE = "tech_stack_change"
COMPETITOR_UPDATE = "competitor_update"
TASK_COMPLETED = "task_completed"
TASK_BLOCKED = "task_blocked"
PROJECT_RISK = "project_risk"
class EventDrivenWorkflowEngine:
"""事件驱动工作流引擎"""
def __init__(self, db: AsyncSession):
self.db = db
self.llm = LLMService()
self.event_bus = get_event_bus()
# 注册工作流处理器
self._handlers = {
WorkflowEventType.HIGH_IMPACT_SIGNAL: self._handle_high_impact_signal,
WorkflowEventType.BLOCKER_DETECTED: self._handle_blocker_detected,
WorkflowEventType.COGNITIVE_CONFLICT: self._handle_cognitive_conflict,
WorkflowEventType.TECH_STACK_CHANGE: self._handle_tech_stack_change,
WorkflowEventType.COMPETITOR_UPDATE: self._handle_competitor_update,
WorkflowEventType.TASK_COMPLETED: self._handle_task_completed,
WorkflowEventType.TASK_BLOCKED: self._handle_task_blocked,
WorkflowEventType.PROJECT_RISK: self._handle_project_risk,
}
async def trigger_workflow(
self,
event_type: str,
event_data: Dict[str, Any],
tenant_id: int = 1
) -> Dict[str, Any]:
"""
触发工作流
Args:
event_type: 事件类型
event_data: 事件数据
tenant_id: 租户ID
Returns:
处理结果
"""
handler = self._handlers.get(event_type)
if not handler:
return {'status': 'ignored', 'reason': 'Unknown event type'}
try:
result = await handler(event_data, tenant_id)
# 发布到事件总线
await self.event_bus.publish(
EventType.WORKFLOW_TRIGGERED,
{
'workflow_type': event_type,
'result': result,
'timestamp': now_cn_aware().isoformat()
}
)
return {
'status': 'processed',
'event_type': event_type,
'result': result
}
except Exception as e:
return {
'status': 'error',
'event_type': event_type,
'error': str(e)
}
async def _handle_high_impact_signal(
self,
event_data: Dict[str, Any],
tenant_id: int
) -> Dict[str, Any]:
"""
处理高影响信号
动作:
1. 评估对关联项目的影响
2. 调整项目优先级
3. 创建响应任务
4. 通知相关角色
"""
signal_id = event_data.get('signal_id')
impact_threshold = 0.85
# 获取信号
signal_result = await self.db.execute(
select(Signal).where(
Signal.id == signal_id,
Signal.tenant_id == tenant_id
)
)
signal = signal_result.scalar_one_or_none()
if not signal:
raise ValueError(f"Signal {signal_id} not found")
# 获取关联的项目
link_result = await self.db.execute(
select(ProjectSignalLink).where(
ProjectSignalLink.signal_id == signal_id,
ProjectSignalLink.tenant_id == tenant_id
)
)
links = link_result.scalars().all()
affected_projects = []
created_tasks = []
for link in links:
project_result = await self.db.execute(
select(Project).where(Project.id == link.project_id)
)
project = project_result.scalar_one_or_none()
if not project:
continue
# AI 评估影响
impact = await self._assess_signal_impact_on_project(signal, project)
if impact.get('should_adjust_priority', False):
# 调整项目优先级
old_priority = project.priority if hasattr(project, 'priority') else 'medium'
new_priority = impact.get('suggested_priority', 'high')
if hasattr(project, 'priority'):
project.priority = new_priority
await self.db.commit()
affected_projects.append({
'project_id': project.id,
'project_name': project.title,
'old_priority': old_priority,
'new_priority': new_priority,
'reason': impact.get('reason', '')
})
# 创建响应任务
if impact.get('needs_response_task', False):
task = await self._create_response_task(
project=project,
signal=signal,
impact=impact,
tenant_id=tenant_id
)
created_tasks.append({
'task_id': task.id,
'title': task.title,
'priority': task.priority
})
return {
'affected_projects': affected_projects,
'created_tasks': created_tasks,
'message': f'处理了 {len(affected_projects)} 个受影响的项目'
}
async def _handle_blocker_detected(
self,
event_data: Dict[str, Any],
tenant_id: int
) -> Dict[str, Any]:
"""
处理阻塞检测
动作:
1. AI 生成替代方案
2. 创建绕过任务
3. 通知相关人员
"""
blocker_id = event_data.get('blocker_id')
task_id = event_data.get('task_id')
# 获取阻塞信息
conflict_result = await self.db.execute(
select(CognitiveConflict).where(
CognitiveConflict.id == blocker_id,
CognitiveConflict.tenant_id == tenant_id
)
)
blocker = conflict_result.scalar_one_or_none()
if not blocker:
raise ValueError(f"Blocker {blocker_id} not found")
# AI 生成解决方案
prompt = f"""为以下阻塞生成绕过方案:
阻塞描述:{blocker.description}
请提供:
1. 问题根本原因分析
2. 3个可行的绕过方案
3. 每个方案的预估工作量
4. 推荐的最优方案
以 JSON 格式返回。"""
try:
response = await self.llm.generate(
prompt,
db=self.db,
tenant_id=tenant_id,
)
# 解析 AI 响应(简化处理)
return {
'blocker_id': blocker_id,
'alternatives_generated': True,
'ai_response': response
}
except:
return {
'blocker_id': blocker_id,
'alternatives_generated': False,
'message': 'AI 方案生成失败'
}
async def _handle_cognitive_conflict(
self,
event_data: Dict[str, Any],
tenant_id: int
) -> Dict[str, Any]:
"""
处理认知冲突
动作:
1. 识别涉及的用户
2. 发送对齐邀请
3. 调度异步对齐会话
"""
conflict_id = event_data.get('conflict_id')
conflict_result = await self.db.execute(
select(CognitiveConflict).where(
CognitiveConflict.id == conflict_id,
CognitiveConflict.tenant_id == tenant_id
)
)
conflict = conflict_result.scalar_one_or_none()
if not conflict:
raise ValueError(f"Conflict {conflict_id} not found")
# 获取涉及的用户
involved_user_ids = json.loads(conflict.involved_user_ids) if conflict.involved_user_ids else []
return {
'conflict_id': conflict_id,
'involved_users': involved_user_ids,
'alignment_session_created': len(involved_user_ids) >= 2,
'message': '对齐邀请已发送'
}
async def _handle_tech_stack_change(
self,
event_data: Dict[str, Any],
tenant_id: int
) -> Dict[str, Any]:
"""
处理技术栈变更
动作:
1. 识别受影响的任务
2. 更新任务描述和依赖
3. 重新估算工时
"""
project_id = event_data.get('project_id')
new_tech = event_data.get('new_tech', '')
# 获取项目的所有任务
task_result = await self.db.execute(
select(Task).where(
Task.project_id == project_id,
Task.tenant_id == tenant_id,
Task.status.in_(['pending', 'in_progress'])
)
)
tasks = task_result.scalars().all()
affected_tasks = []
for task in tasks:
# AI 判断任务是否受影响
is_affected = await self._is_task_affected_by_tech_change(task, new_tech)
if is_affected:
affected_tasks.append({
'task_id': task.id,
'task_title': task.title,
'impact': 'high'
})
return {
'project_id': project_id,
'new_tech': new_tech,
'affected_tasks': affected_tasks,
'message': f'识别出 {len(affected_tasks)} 个受影响的任务'
}
async def _handle_competitor_update(
self,
event_data: Dict[str, Any],
tenant_id: int
) -> Dict[str, Any]:
"""
处理竞品动态
动作:
1. 评估战略影响
2. 调整项目方向
3. 创建应对任务
"""
signal_content = event_data.get('signal_content', '')
competitor_name = event_data.get('competitor_name', '竞品')
# AI 分析影响
prompt = f"""分析以下竞品动态的战略影响:
竞品:{competitor_name}
动态:{signal_content}
请评估:
1. 对我们的威胁等级(high/medium/low)
2. 建议的应对措施
3. 是否需要调整项目方向
以 JSON 格式返回。"""
try:
response = await self.llm.generate(
prompt,
db=self.db,
tenant_id=tenant_id,
)
return {
'competitor': competitor_name,
'threat_assessment': response,
'strategic_adjustment_recommended': True
}
except:
return {
'competitor': competitor_name,
'threat_assessment': 'medium',
'message': 'AI 评估失败'
}
async def _handle_task_completed(
self,
event_data: Dict[str, Any],
tenant_id: int
) -> Dict[str, Any]:
"""
处理任务完成
动作:
1. 检查依赖任务,解锁可开始的
2. 更新项目进度
3. 触发后续任务
"""
task_id = event_data.get('task_id')
# 获取任务
task_result = await self.db.execute(
select(Task).where(
Task.id == task_id,
Task.tenant_id == tenant_id
)
)
task = task_result.scalar_one_or_none()
if not task:
raise ValueError(f"Task {task_id} not found")
# 查找依赖此任务的其他任务
# 这里简化处理:查找同项目的待办任务
dependent_result = await self.db.execute(
select(Task).where(
Task.project_id == task.project_id,
Task.tenant_id == tenant_id,
Task.status == 'pending',
Task.id != task_id
).limit(5)
)
dependents = dependent_result.scalars().all()
return {
'completed_task_id': task_id,
'project_id': task.project_id,
'unlocked_tasks': [
{'id': t.id, 'title': t.title}
for t in dependents
],
'message': f'解锁了 {len(dependents)} 个待办任务'
}
async def _handle_task_blocked(
self,
event_data: Dict[str, Any],
tenant_id: int
) -> Dict[str, Any]:
"""
处理任务阻塞
动作:
1. 标记任务状态
2. 触发阻塞检测工作流
3. 通知项目负责人
"""
task_id = event_data.get('task_id')
blocker_reason = event_data.get('reason', 'Unknown')
# 更新任务状态
await self.db.execute(
update(Task)
.where(Task.id == task_id, Task.tenant_id == tenant_id)
.values(status='blocked')
)
await self.db.commit()
return {
'task_id': task_id,
'blocker_reason': blocker_reason,
'status': 'blocked',
'workflow_triggered': True
}
async def _handle_project_risk(
self,
event_data: Dict[str, Any],
tenant_id: int
) -> Dict[str, Any]:
"""
处理项目风险
动作:
1. 评估风险等级
2. 创建缓解任务
3. 通知管理层
"""
project_id = event_data.get('project_id')
risk_type = event_data.get('risk_type', 'general')
risk_description = event_data.get('description', '')
# 获取项目
project_result = await self.db.execute(
select(Project).where(
Project.id == project_id,
Project.tenant_id == tenant_id
)
)
project = project_result.scalar_one_or_none()
if not project:
raise ValueError(f"Project {project_id} not found")
return {
'project_id': project_id,
'project_name': project.title if project else 'Unknown',
'risk_type': risk_type,
'risk_level': 'high',
'mitigation_required': True,
'message': '风险已记录,等待处理'
}
# ==================== Helper Methods ====================
async def _assess_signal_impact_on_project(
self,
signal: Signal,
project: Project
) -> Dict[str, Any]:
"""AI 评估信号对项目的影响"""
prompt = f"""评估以下信号对项目的影响:
项目:{project.title}
描述:{project.description}
信号:
来源:{signal.source}
内容:{signal.content}
影响分数:{signal.impact_score}
请判断:
1. 是否需要调整项目优先级?
2. 建议的优先级是什么?
3. 是否需要创建响应任务?
4. 原因是什么?
以 JSON 格式返回:
{{
"should_adjust_priority": boolean,
"suggested_priority": "high/medium/low",
"needs_response_task": boolean,
"reason": "原因说明"
}}"""
try:
response = await self.llm.generate(
prompt,
db=self.db,
tenant_id=signal.tenant_id if getattr(signal, "tenant_id", None) is not None else None,
project_id=project.id if project else None,
)
response = response.strip()
if response.startswith('```'):
response = response.split('```')[1]
if response.startswith('json'):
response = response[4:]
return json.loads(response.strip())
except:
return {
'should_adjust_priority': signal.impact_score > 0.9,
'suggested_priority': 'high' if signal.impact_score > 0.9 else 'medium',
'needs_response_task': signal.impact_score > 0.85,
'reason': f'信号影响分数 {signal.impact_score}'
}
async def _create_response_task(
self,
project: Project,
signal: Signal,
impact: Dict[str, Any],
tenant_id: int
):
"""创建响应任务"""
from sqlalchemy import text
next_task_id = (await self.db.execute(text("SELECT COALESCE(MAX(id),0)+1 FROM tasks"))).scalar() or 1
task = Task(
id=next_task_id,
truth_node_id=None,
project_id=project.id,
assignee_id=None,
title=f'响应信号: {signal.content[:50]}',
description=f"""自动生成的响应任务
触发信号:
来源:{signal.source}
内容:{signal.content}
影响评估:{impact.get('reason', '')}
建议行动:
请根据信号内容制定具体的响应方案。""",
required_skill_tag_ids=[],
status='pending',
priority='HIGH',
estimated_hours=8,
actual_hours=0,
quality_score=0,
tenant_id=tenant_id
)
self.db.add(task)
await self.db.commit()
await self.db.refresh(task)
return task
async def _is_task_affected_by_tech_change(
self,
task: Task,
new_tech: str
) -> bool:
"""判断任务是否受技术变更影响"""
# 简化处理:检查任务描述中是否包含相关技术关键词
task_keywords = task.title.lower() + ' ' + task.description.lower()
# 这里可以更复杂,使用 AI 判断
return new_tech.lower() in task_keywords
# 全局服务实例
def get_event_workflow_engine(db: AsyncSession) -> EventDrivenWorkflowEngine:
"""获取事件驱动工作流引擎实例"""
return EventDrivenWorkflowEngine(db)