482 lines
16 KiB
Python
482 lines
16 KiB
Python
"""
|
||
Daily Progress Briefing Service - 每日进度简报服务
|
||
|
||
核心功能:
|
||
1. 为每个角色生成个性化简报
|
||
2. 智能过滤无关信息
|
||
3. 根据角色优先级排序内容
|
||
4. 替代传统日报/周报
|
||
|
||
个性化规则:
|
||
- AI工程师:关注模型更新、技术信号、实验结果
|
||
- 后端开发:关注API变更、任务阻塞、依赖更新
|
||
- 前端开发:关注UI需求、设计变更、接口更新
|
||
- BOSS/ADMIN:关注整体进度、风险、决策点
|
||
"""
|
||
import asyncio
|
||
import json
|
||
from datetime import datetime, timedelta, timezone
|
||
from typing import Any, Dict, List, Optional
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy.future import select
|
||
from sqlalchemy import text, func, and_
|
||
|
||
from ..models import (
|
||
User, Project, Task, Signal, CognitiveConflict,
|
||
TruthNode, UserRole, ProjectSignalLink
|
||
)
|
||
from .llm import LLMService
|
||
|
||
|
||
from ..utils.timezone import now_cn_aware
|
||
def _parse_involved_user_ids(raw_value: Any) -> List[int]:
|
||
"""兼容 involved_user_ids 的 list / json-string / 单值字符串。"""
|
||
if raw_value is None:
|
||
return []
|
||
data = raw_value
|
||
if isinstance(data, (bytes, bytearray)):
|
||
data = data.decode("utf-8", errors="ignore")
|
||
if isinstance(data, str):
|
||
text = data.strip()
|
||
if not text:
|
||
return []
|
||
try:
|
||
data = json.loads(text)
|
||
except (TypeError, ValueError, json.JSONDecodeError):
|
||
data = [v.strip() for v in text.split(",") if v.strip()]
|
||
if not isinstance(data, list):
|
||
data = [data]
|
||
|
||
parsed: List[int] = []
|
||
for item in data:
|
||
if item is None or isinstance(item, bool):
|
||
continue
|
||
try:
|
||
parsed.append(int(item))
|
||
except (TypeError, ValueError):
|
||
continue
|
||
return parsed
|
||
|
||
|
||
class DailyBriefingService:
|
||
"""每日进度简报服务"""
|
||
|
||
def __init__(self, db: AsyncSession):
|
||
self.db = db
|
||
self.llm = LLMService()
|
||
|
||
async def generate_personalized_briefing(
|
||
self,
|
||
user_id: int,
|
||
tenant_id: int = 1
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
生成个性化每日简报
|
||
|
||
简报内容根据用户角色自动过滤:
|
||
- 只显示相关项目的内容
|
||
- 按优先级排序
|
||
- 突出风险和需要行动的事项
|
||
"""
|
||
# 获取用户信息
|
||
user_result = await self.db.execute(
|
||
select(User).where(User.id == user_id)
|
||
)
|
||
user = user_result.scalar_one_or_none()
|
||
if not user:
|
||
raise ValueError(f"User {user_id} not found")
|
||
|
||
# 计算时间范围
|
||
now = now_cn_aware()
|
||
yesterday = now - timedelta(days=1)
|
||
|
||
# 根据角色收集相关信息
|
||
relevant_data = await self._collect_relevant_data(
|
||
user=user,
|
||
since=yesterday,
|
||
tenant_id=tenant_id
|
||
)
|
||
|
||
# AI 生成个性化摘要
|
||
summary = await self._generate_ai_summary(
|
||
user=user,
|
||
data=relevant_data,
|
||
tenant_id=tenant_id,
|
||
)
|
||
|
||
return {
|
||
'user_id': user_id,
|
||
'user_role': self._role_code(user.role),
|
||
'date': now.strftime('%Y-%m-%d'),
|
||
'summary': summary,
|
||
'highlights': relevant_data.get('highlights', []),
|
||
'your_tasks': relevant_data.get('your_tasks', []),
|
||
'risks': relevant_data.get('risks', []),
|
||
'signals': relevant_data.get('signals', []),
|
||
'conflicts': relevant_data.get('conflicts', []),
|
||
'action_items': relevant_data.get('action_items', []),
|
||
'team_updates': relevant_data.get('team_updates', [])
|
||
}
|
||
|
||
async def _collect_relevant_data(
|
||
self,
|
||
user: User,
|
||
since: datetime,
|
||
tenant_id: int
|
||
) -> Dict[str, Any]:
|
||
"""收集与用户相关的数据"""
|
||
|
||
data = {
|
||
'highlights': [],
|
||
'your_tasks': [],
|
||
'risks': [],
|
||
'signals': [],
|
||
'conflicts': [],
|
||
'action_items': [],
|
||
'team_updates': []
|
||
}
|
||
|
||
# 获取用户相关的项目
|
||
user_projects = await self._get_user_projects(user.id, tenant_id)
|
||
|
||
if not user_projects:
|
||
return data
|
||
|
||
project_ids = [p.id for p in user_projects]
|
||
role_code = self._role_code(user.role)
|
||
|
||
# 1. 获取用户的任务更新
|
||
task_result = await self.db.execute(
|
||
select(Task, Project).join(
|
||
Project,
|
||
and_(Project.id == Task.project_id, Project.tenant_id == tenant_id),
|
||
).where(
|
||
Task.tenant_id == tenant_id,
|
||
Task.project_id.in_(project_ids),
|
||
Task.updated_at >= since
|
||
)
|
||
)
|
||
task_rows = task_result.all()
|
||
|
||
for task, project in task_rows:
|
||
# 我的任务
|
||
if task.assignee_id == user.id:
|
||
data['your_tasks'].append({
|
||
'id': task.id,
|
||
'title': task.title,
|
||
'status': task.status,
|
||
'priority': task.priority,
|
||
'project': project.title,
|
||
'updated_at': task.updated_at.isoformat() if hasattr(task, 'updated_at') else None
|
||
})
|
||
|
||
# 高优先级或阻塞的任务加入 highlights
|
||
if task.priority == 'HIGH' or task.status == 'blocked':
|
||
data['highlights'].append({
|
||
'type': 'task',
|
||
'priority': 'high',
|
||
'content': f'任务"{task.title}"状态: {task.status}',
|
||
'project': project.title
|
||
})
|
||
|
||
# 2. 获取风险(阻塞的任务)
|
||
for task, project in task_rows:
|
||
if task.status == 'blocked':
|
||
data['risks'].append({
|
||
'type': 'task_blocked',
|
||
'task': task.title,
|
||
'project': project.title,
|
||
'severity': 'high'
|
||
})
|
||
|
||
# 3. 获取新信号(根据角色过滤)
|
||
signal_result = await self.db.execute(
|
||
select(Signal).join(
|
||
ProjectSignalLink,
|
||
ProjectSignalLink.signal_id == Signal.id
|
||
).where(
|
||
Signal.tenant_id == tenant_id,
|
||
ProjectSignalLink.project_id.in_(project_ids),
|
||
Signal.created_at >= since,
|
||
Signal.impact_score >= 0.7 # 只显示高影响信号
|
||
).order_by(
|
||
Signal.impact_score.desc()
|
||
).limit(10)
|
||
)
|
||
signals = signal_result.scalars().all()
|
||
|
||
for signal in signals:
|
||
# 根据角色过滤信号内容
|
||
if self._is_signal_relevant_to_role(signal, role_code):
|
||
data['signals'].append({
|
||
'id': signal.id,
|
||
'source': signal.source,
|
||
'content': signal.content[:200],
|
||
'impact_score': signal.impact_score,
|
||
'priority': signal.priority
|
||
})
|
||
|
||
# 高影响信号加入 highlights
|
||
if signal.impact_score >= 0.9:
|
||
data['highlights'].append({
|
||
'type': 'signal',
|
||
'priority': 'high',
|
||
'content': f'高影响信号: {signal.content[:100]}',
|
||
'source': signal.source
|
||
})
|
||
|
||
# 4. 获取认知冲突
|
||
conflict_result = await self.db.execute(
|
||
select(CognitiveConflict).join(
|
||
TruthNode,
|
||
TruthNode.id == CognitiveConflict.truth_node_id
|
||
).where(
|
||
CognitiveConflict.tenant_id == tenant_id,
|
||
TruthNode.project_id.in_(project_ids),
|
||
CognitiveConflict.status == 'active',
|
||
CognitiveConflict.created_at >= since
|
||
)
|
||
)
|
||
conflicts = conflict_result.scalars().all()
|
||
|
||
for conflict in conflicts:
|
||
involved_users = _parse_involved_user_ids(conflict.involved_user_ids)
|
||
# 如果用户参与了冲突,或是管理员
|
||
if user.id in involved_users or role_code in [UserRole.ADMIN.value, UserRole.BOSS.value]:
|
||
data['conflicts'].append({
|
||
'id': conflict.id,
|
||
'type': conflict.conflict_type,
|
||
'description': conflict.description[:200],
|
||
'severity': 'high' if 'critical' in conflict.description.lower() else 'medium'
|
||
})
|
||
|
||
# 冲突加入 highlights
|
||
data['highlights'].append({
|
||
'type': 'conflict',
|
||
'priority': 'high',
|
||
'content': f'认知冲突: {conflict.conflict_type}',
|
||
'project_id': conflict.truth_node_id
|
||
})
|
||
|
||
# 5. 生成行动项
|
||
if data['your_tasks']:
|
||
for task in data['your_tasks'][:3]:
|
||
if task['status'] in ['pending', 'in_progress']:
|
||
data['action_items'].append({
|
||
'type': 'task',
|
||
'content': f'继续推进任务: {task["title"]}',
|
||
'priority': task['priority']
|
||
})
|
||
|
||
if data['conflicts']:
|
||
data['action_items'].append({
|
||
'type': 'conflict',
|
||
'content': f'解决 {len(data["conflicts"])} 个认知冲突',
|
||
'priority': 'high'
|
||
})
|
||
|
||
# 6. 团队更新(其他用户的相关任务)
|
||
team_task_result = await self.db.execute(
|
||
select(Task, Project).join(
|
||
Project,
|
||
and_(Project.id == Task.project_id, Project.tenant_id == tenant_id),
|
||
).where(
|
||
Task.tenant_id == tenant_id,
|
||
Task.project_id.in_(project_ids),
|
||
Task.assignee_id != user.id,
|
||
Task.updated_at >= since
|
||
)
|
||
)
|
||
team_tasks = team_task_result.all()
|
||
|
||
for task, project in team_tasks[:5]:
|
||
data['team_updates'].append({
|
||
'assignee_id': task.assignee_id,
|
||
'task': task.title,
|
||
'status': task.status,
|
||
'project': project.title
|
||
})
|
||
|
||
return data
|
||
|
||
async def _get_user_projects(
|
||
self,
|
||
user_id: int,
|
||
tenant_id: int
|
||
) -> List[Project]:
|
||
"""获取用户相关的项目"""
|
||
# 获取用户参与的所有项目
|
||
result = await self.db.execute(
|
||
select(Project).where(
|
||
Project.tenant_id == tenant_id,
|
||
Project.status == 'active'
|
||
)
|
||
)
|
||
return result.scalars().all()
|
||
|
||
def _is_signal_relevant_to_role(
|
||
self,
|
||
signal: Signal,
|
||
user_role: Any
|
||
) -> bool:
|
||
"""判断信号是否与用户角色相关"""
|
||
content = signal.content.lower()
|
||
role_code = self._role_code(user_role)
|
||
_ = signal.source.lower()
|
||
|
||
# AI工程师关注的内容
|
||
if role_code == UserRole.AI.value:
|
||
ai_keywords = ['model', 'llm', 'gpt', 'embedding', 'vector', '训练', '推理', 'ollama', 'langchain']
|
||
if any(kw in content for kw in ai_keywords):
|
||
return True
|
||
|
||
# 后端开发关注的内容
|
||
elif role_code in {UserRole.BE.value, "BACKEND"}:
|
||
backend_keywords = ['api', 'backend', 'server', 'database', 'sql', '后端', '接口', '服务']
|
||
if any(kw in content for kw in backend_keywords):
|
||
return True
|
||
|
||
# 前端开发关注的内容
|
||
elif role_code in {UserRole.FE.value, "FRONTEND"}:
|
||
frontend_keywords = ['ui', 'frontend', 'react', 'vue', 'component', '前端', '界面', '交互']
|
||
if any(kw in content for kw in frontend_keywords):
|
||
return True
|
||
|
||
# 管理员看到所有高影响信号
|
||
elif role_code in {UserRole.ADMIN.value, UserRole.BOSS.value}:
|
||
return signal.impact_score >= 0.8
|
||
|
||
# 默认返回中等影响以上的信号
|
||
return signal.impact_score >= 0.7
|
||
|
||
async def _generate_ai_summary(
|
||
self,
|
||
user: User,
|
||
data: Dict[str, Any],
|
||
tenant_id: int,
|
||
) -> str:
|
||
"""AI 生成个性化摘要"""
|
||
role_code = self._role_code(user.role)
|
||
prompt = f"""为以下用户生成每日工作摘要:
|
||
|
||
用户角色:{role_code}
|
||
用户名:{user.username}
|
||
|
||
今日概况:
|
||
- 你的任务数:{len(data.get('your_tasks', []))}
|
||
- 新信号数:{len(data.get('signals', []))}
|
||
- 活跃冲突数:{len(data.get('conflicts', []))}
|
||
- 风险数:{len(data.get('risks', []))}
|
||
|
||
重点事项:
|
||
{json.dumps(data.get('highlights', []), ensure_ascii=False, indent=2)[:500]}
|
||
|
||
请生成一段简洁的每日摘要(100字以内),突出最重要的信息和行动项。"""
|
||
|
||
try:
|
||
summary = await self.llm.generate(
|
||
prompt,
|
||
db=self.db,
|
||
tenant_id=tenant_id,
|
||
)
|
||
return summary.strip()
|
||
except:
|
||
# 降级:生成简单摘要
|
||
parts = []
|
||
if data.get('your_tasks'):
|
||
parts.append(f"你有 {len(data['your_tasks'])} 个任务需要关注")
|
||
if data.get('risks'):
|
||
parts.append(f"注意 {len(data['risks'])} 个风险点")
|
||
if data.get('conflicts'):
|
||
parts.append(f"需要解决 {len(data['conflicts'])} 个认知冲突")
|
||
return ";".join(parts) if parts else "今日无重要更新"
|
||
|
||
@staticmethod
|
||
def _role_code(raw_role: Any) -> str:
|
||
if raw_role is None:
|
||
return ""
|
||
value = getattr(raw_role, "value", raw_role)
|
||
return str(value or "").strip().upper()
|
||
|
||
async def generate_team_briefing(
|
||
self,
|
||
project_id: int,
|
||
tenant_id: int = 1
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
生成团队简报(给管理者)
|
||
|
||
包含:
|
||
- 整体进度
|
||
- 每个成员的工作量
|
||
- 风险和阻塞
|
||
- 需要决策的事项
|
||
"""
|
||
# 获取项目信息
|
||
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")
|
||
|
||
# 获取项目所有任务
|
||
task_result = await self.db.execute(
|
||
select(Task).where(
|
||
Task.project_id == project_id,
|
||
Task.tenant_id == tenant_id
|
||
)
|
||
)
|
||
tasks = task_result.scalars().all()
|
||
|
||
# 统计任务状态
|
||
status_counts = {}
|
||
for task in tasks:
|
||
status_counts[task.status] = status_counts.get(task.status, 0) + 1
|
||
|
||
# 获取阻塞的任务
|
||
blocked_tasks = [t for t in tasks if t.status == 'blocked']
|
||
|
||
# 获取项目相关信号
|
||
signal_result = await self.db.execute(
|
||
select(Signal).join(
|
||
ProjectSignalLink,
|
||
ProjectSignalLink.signal_id == Signal.id
|
||
).where(
|
||
Signal.tenant_id == tenant_id,
|
||
ProjectSignalLink.project_id == project_id
|
||
).order_by(
|
||
Signal.created_at.desc()
|
||
).limit(10)
|
||
)
|
||
signals = signal_result.scalars().all()
|
||
|
||
return {
|
||
'project_id': project_id,
|
||
'project_name': project.title,
|
||
'generated_at': now_cn_aware().isoformat(),
|
||
'task_summary': {
|
||
'total': len(tasks),
|
||
'by_status': status_counts,
|
||
'completion_rate': status_counts.get('done', 0) / len(tasks) if tasks else 0
|
||
},
|
||
'blocked_tasks': [
|
||
{'id': t.id, 'title': t.title, 'assignee_id': t.assignee_id}
|
||
for t in blocked_tasks
|
||
],
|
||
'recent_signals': [
|
||
{'id': s.id, 'content': s.content[:100], 'impact_score': s.impact_score}
|
||
for s in signals[:5]
|
||
],
|
||
'needs_attention': len(blocked_tasks) > 0
|
||
}
|
||
|
||
|
||
# 全局服务实例
|
||
def get_daily_briefing_service(db: AsyncSession) -> DailyBriefingService:
|
||
"""获取每日简报服务实例"""
|
||
return DailyBriefingService(db)
|