Files
multiAgentTry/backend/app/services/meeting_recorder.py

405 lines
12 KiB
Python
Raw Normal View History

"""
会议记录服务 - 记录会议内容讨论和共识
将会议记录保存为 Markdown 文件按日期组织
"""
import asyncio
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
from dataclasses import dataclass, field, asdict
from .storage import get_storage
@dataclass
class DiscussionEntry:
"""单条讨论记录"""
agent_id: str
agent_name: str
content: str
timestamp: str = ""
step: str = ""
def __post_init__(self):
if not self.timestamp:
self.timestamp = datetime.now().isoformat()
@dataclass
class ProgressStep:
"""会议进度步骤"""
step_id: str
label: str
status: str = "pending" # pending, active, completed
completed_at: str = ""
def mark_active(self):
self.status = "active"
def mark_completed(self):
self.status = "completed"
self.completed_at = datetime.now().isoformat()
@dataclass
class MeetingInfo:
"""会议信息"""
meeting_id: str
title: str
date: str # YYYY-MM-DD
attendees: List[str] = field(default_factory=list)
steps: List[ProgressStep] = field(default_factory=list)
discussions: List[DiscussionEntry] = field(default_factory=list)
status: str = "in_progress" # in_progress, completed
created_at: str = ""
ended_at: str = ""
consensus: str = "" # 最终共识
def __post_init__(self):
if not self.created_at:
self.created_at = datetime.now().isoformat()
if not self.date:
self.date = datetime.now().strftime("%Y-%m-%d")
@property
def current_step(self) -> Optional[ProgressStep]:
"""获取当前活跃的步骤"""
for step in self.steps:
if step.status == "active":
return step
return None
@property
def completed_steps(self) -> List[ProgressStep]:
"""获取已完成的步骤"""
return [s for s in self.steps if s.status == "completed"]
@property
def progress_summary(self) -> str:
"""进度摘要"""
total = len(self.steps)
if total == 0:
return "0/0"
completed = len(self.completed_steps)
active = 1 if self.current_step else 0
return f"{completed}/{total}" + (" (active)" if active else "")
class MeetingRecorder:
"""
会议记录服务
记录会议的讨论内容进度和共识保存为 Markdown 文件
"""
def __init__(self):
self._storage = get_storage()
self._lock = asyncio.Lock()
def _parse_meeting_data(self, data: dict) -> MeetingInfo:
"""将字典数据转换为 MeetingInfo 对象"""
# 转换 steps
steps = [
ProgressStep(**s) if isinstance(s, dict) else s
for s in data.get("steps", [])
]
# 转换 discussions
discussions = [
DiscussionEntry(**d) if isinstance(d, dict) else d
for d in data.get("discussions", [])
]
# 创建 MeetingInfo
data["steps"] = steps
data["discussions"] = discussions
return MeetingInfo(**data)
def _get_meeting_dir(self, date: str) -> str:
"""获取会议目录路径"""
return f"meetings/{date}"
def _get_meeting_file(self, meeting_id: str, date: str) -> str:
"""获取会议文件路径"""
return f"{self._get_meeting_dir(date)}/{meeting_id}.md"
async def create_meeting(
self,
meeting_id: str,
title: str,
attendees: List[str],
steps: List[str] = None
) -> MeetingInfo:
"""
创建新会议记录
Args:
meeting_id: 会议 ID
title: 会议标题
attendees: 参会者列表
steps: 会议步骤列表
Returns:
创建的会议信息
"""
async with self._lock:
date = datetime.now().strftime("%Y-%m-%d")
# 创建进度步骤
progress_steps = []
if steps:
for i, step_label in enumerate(steps):
progress_steps.append(ProgressStep(
step_id=f"step_{i+1}",
label=step_label,
status="pending"
))
meeting = MeetingInfo(
meeting_id=meeting_id,
title=title,
date=date,
attendees=attendees,
steps=progress_steps
)
# 保存为 Markdown
await self._save_meeting_markdown(meeting)
return meeting
async def _save_meeting_markdown(self, meeting: MeetingInfo) -> None:
"""将会议保存为 Markdown 文件"""
lines = [
f"# {meeting.title}",
"",
f"**会议 ID**: {meeting.meeting_id}",
f"**日期**: {meeting.date}",
f"**状态**: {meeting.status}",
f"**参会者**: {', '.join(meeting.attendees)}",
"",
"## 会议进度",
"",
]
# 进度步骤
for step in meeting.steps:
status_icon = {
"pending": "",
"active": "",
"completed": ""
}.get(step.status, "")
time_str = f" ({step.completed_at})" if step.completed_at else ""
lines.append(f"- {status_icon} **{step.label}**{time_str}")
lines.append("")
lines.append("## 讨论记录")
lines.append("")
# 讨论内容
for discussion in meeting.discussions:
lines.append(f"### {discussion.agent_name} - {discussion.timestamp[:19]}")
if discussion.step:
lines.append(f"*步骤: {discussion.step}*")
lines.append("")
lines.append(discussion.content)
lines.append("")
# 共识
if meeting.consensus:
lines.append("## 共识")
lines.append("")
lines.append(meeting.consensus)
lines.append("")
# 元数据
lines.append("---")
lines.append("")
lines.append(f"**创建时间**: {meeting.created_at}")
if meeting.ended_at:
lines.append(f"**结束时间**: {meeting.ended_at}")
content = "\n".join(lines)
file_path = self._get_meeting_file(meeting.meeting_id, meeting.date)
# 使用存储服务保存
await self._storage.ensure_dir(self._get_meeting_dir(meeting.date))
await self._storage.write_json(file_path.replace(".md", ".json"), asdict(meeting))
# 同时保存 Markdown
import aiofiles
full_path = Path(self._storage.base_path) / file_path
async with aiofiles.open(full_path, mode="w", encoding="utf-8") as f:
await f.write(content)
async def add_discussion(
self,
meeting_id: str,
agent_id: str,
agent_name: str,
content: str,
step: str = ""
) -> None:
"""
添加讨论记录
Args:
meeting_id: 会议 ID
agent_id: Agent ID
agent_name: Agent 名称
content: 讨论内容
step: 当前步骤
"""
async with self._lock:
# 加载会议信息
date = datetime.now().strftime("%Y-%m-%d")
file_path = self._get_meeting_file(meeting_id, date)
json_path = file_path.replace(".md", ".json")
data = await self._storage.read_json(json_path)
if not data:
return # 会议不存在
meeting = self._parse_meeting_data(data)
meeting.discussions.append(DiscussionEntry(
agent_id=agent_id,
agent_name=agent_name,
content=content,
step=step
))
# 保存
await self._save_meeting_markdown(meeting)
async def update_progress(
self,
meeting_id: str,
step_label: str
) -> None:
"""
更新会议进度
Args:
meeting_id: 会议 ID
step_label: 步骤名称
"""
async with self._lock:
date = datetime.now().strftime("%Y-%m-%d")
json_path = self._get_meeting_file(meeting_id, date).replace(".md", ".json")
data = await self._storage.read_json(json_path)
if not data:
return
meeting = self._parse_meeting_data(data)
# 查找并更新步骤
step_found = False
for step in meeting.steps:
if step.label == step_label:
# 将之前的活跃步骤标记为完成
if meeting.current_step:
meeting.current_step.mark_completed()
step.mark_active()
step_found = True
break
if not step_found and meeting.steps:
# 如果找不到,将第一个 pending 步骤设为活跃
for step in meeting.steps:
if step.status == "pending":
if meeting.current_step:
meeting.current_step.mark_completed()
step.mark_active()
break
await self._save_meeting_markdown(meeting)
async def get_meeting(self, meeting_id: str, date: str = None) -> Optional[MeetingInfo]:
"""
获取会议信息
Args:
meeting_id: 会议 ID
date: 日期默认为今天
Returns:
会议信息
"""
if date is None:
date = datetime.now().strftime("%Y-%m-%d")
json_path = self._get_meeting_file(meeting_id, date).replace(".md", ".json")
data = await self._storage.read_json(json_path)
if data:
return self._parse_meeting_data(data)
return None
async def list_meetings(self, date: str = None) -> List[MeetingInfo]:
"""
列出指定日期的会议
Args:
date: 日期默认为今天
Returns:
会议列表
"""
if date is None:
date = datetime.now().strftime("%Y-%m-%d")
meetings_dir = Path(self._storage.base_path) / self._get_meeting_dir(date)
if not meetings_dir.exists():
return []
meetings = []
for json_file in meetings_dir.glob("*.json"):
data = await self._storage.read_json(f"meetings/{date}/{json_file.name}")
if data:
meetings.append(self._parse_meeting_data(data))
return sorted(meetings, key=lambda m: m.created_at)
async def end_meeting(self, meeting_id: str, consensus: str = "") -> bool:
"""
结束会议
Args:
meeting_id: 会议 ID
consensus: 最终共识
Returns:
是否成功
"""
async with self._lock:
date = datetime.now().strftime("%Y-%m-%d")
json_path = self._get_meeting_file(meeting_id, date).replace(".md", ".json")
data = await self._storage.read_json(json_path)
if not data:
return False
meeting = self._parse_meeting_data(data)
meeting.status = "completed"
meeting.ended_at = datetime.now().isoformat()
if consensus:
meeting.consensus = consensus
# 完成当前步骤
if meeting.current_step:
meeting.current_step.mark_completed()
await self._save_meeting_markdown(meeting)
return True
# 全局单例
_recorder_instance: Optional[MeetingRecorder] = None
def get_meeting_recorder() -> MeetingRecorder:
"""获取会议记录服务单例"""
global _recorder_instance
if _recorder_instance is None:
_recorder_instance = MeetingRecorder()
return _recorder_instance