616 lines
18 KiB
Python
616 lines
18 KiB
Python
"""
|
||
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)
|