Skip to main content
Glama
report_writing_agent.py13.7 kB
"""보고서 작성 에이전트: 수집된 정보를 바탕으로 구조화된 보고서를 생성하는 에이전트""" from typing import Any, ClassVar, List, Dict, Literal from datetime import datetime from langchain_core.language_models.chat_models import BaseChatModel from langchain_core.runnables import RunnableConfig from langchain_core.messages import HumanMessage, AIMessage from langgraph.checkpoint.base import BaseCheckpointSaver from langgraph.graph import StateGraph, END from langgraph.store.base import BaseStore from pydantic import BaseModel, Field from agents.base import BaseAgent, BaseState class ReportSection(BaseModel): """보고서 섹션""" title: str = Field(description="섹션 제목") content: str = Field(description="섹션 내용") order: int = Field(description="섹션 순서") subsections: List['ReportSection'] = Field(default_factory=list, description="하위 섹션") class ReportWritingState(BaseState): """보고서 작성 에이전트의 상태""" topic: str = Field(description="보고서 주제") research_data: str = Field(description="조사된 데이터") report_outline: List[str] = Field(default_factory=list, description="보고서 개요") sections: List[ReportSection] = Field(default_factory=list, description="보고서 섹션") draft_report: str = Field(default="", description="초안") final_report: str = Field(default="", description="최종 보고서") citations: List[str] = Field(default_factory=list, description="인용 목록") report_metadata: Dict[str, Any] = Field(default_factory=dict, description="보고서 메타데이터") quality_score: float = Field(default=0.0, description="품질 점수") needs_revision: bool = Field(default=False, description="수정 필요 여부") class ReportWritingAgent(BaseAgent): """보고서 작성 에이전트""" NODE_NAMES: ClassVar[dict[str, str]] = { "STRUCTURE": "structure_content", "WRITE_SECTIONS": "write_sections", "COMPILE_DRAFT": "compile_draft", "REVIEW_QUALITY": "review_quality", "FINALIZE": "finalize_report", } def __init__( self, model: BaseChatModel, state_schema: Any = ReportWritingState, config_schema: Any | None = None, input_schema: Any | None = None, output_schema: Any | None = None, checkpointer: BaseCheckpointSaver | None = None, store: BaseStore | None = None, max_retry_attempts: int = 2, agent_name: str = "ReportWritingAgent", is_debug: bool = True, ) -> None: super().__init__( model=model, state_schema=state_schema, config_schema=config_schema, input_schema=input_schema, output_schema=output_schema, checkpointer=checkpointer, store=store, max_retry_attempts=max_retry_attempts, agent_name=agent_name, is_debug=is_debug, ) def init_nodes(self, graph: StateGraph): """그래프에 노드 초기화""" structure_node = self.get_node_name("STRUCTURE") write_sections_node = self.get_node_name("WRITE_SECTIONS") compile_draft_node = self.get_node_name("COMPILE_DRAFT") review_quality_node = self.get_node_name("REVIEW_QUALITY") finalize_node = self.get_node_name("FINALIZE") graph.add_node(structure_node, self.structure_content) graph.add_node(write_sections_node, self.write_sections) graph.add_node(compile_draft_node, self.compile_draft) graph.add_node(review_quality_node, self.review_quality) graph.add_node(finalize_node, self.finalize_report) def init_edges(self, graph: StateGraph): """그래프에 엣지 초기화""" structure_node = self.get_node_name("STRUCTURE") write_sections_node = self.get_node_name("WRITE_SECTIONS") compile_draft_node = self.get_node_name("COMPILE_DRAFT") review_quality_node = self.get_node_name("REVIEW_QUALITY") finalize_node = self.get_node_name("FINALIZE") # 워크플로우 정의 graph.set_entry_point(structure_node) graph.add_edge(structure_node, write_sections_node) graph.add_edge(write_sections_node, compile_draft_node) graph.add_edge(compile_draft_node, review_quality_node) # 조건부 엣지: 품질 검토 후 수정 필요 여부에 따라 분기 graph.add_conditional_edges( review_quality_node, self.should_revise, { "revise": write_sections_node, "finalize": finalize_node, } ) graph.add_edge(finalize_node, END) async def structure_content(self, state: ReportWritingState, config: RunnableConfig) -> ReportWritingState: """콘텐츠 구조화 및 개요 작성""" try: prompt = f"""다음 주제와 조사 데이터를 바탕으로 체계적인 보고서 개요를 작성하세요: 주제: {state.topic} 조사 데이터: {state.research_data[:2000]}... 다음 형식으로 보고서 개요를 작성하세요: 1. 제목 2. 요약 (Executive Summary) 3. 서론 4. 본론 (2-4개의 주요 섹션) 5. 결론 6. 권고사항 (해당하는 경우) 7. 참고문헌 각 섹션의 제목과 간단한 설명을 포함하세요.""" response = await self.model.ainvoke([HumanMessage(content=prompt)], config) # 개요 파싱 outline_lines = response.content.strip().split('\n') state.report_outline = [line.strip() for line in outline_lines if line.strip()] # 메타데이터 설정 state.report_metadata = { "created_at": datetime.now().isoformat(), "author": self.agent_name, "version": "1.0", "topic": state.topic } if self.is_debug: print(f"[{self.agent_name}] 보고서 구조화 완료:") print(f" 섹션 수: {len(state.report_outline)}") return state except Exception as e: if self.is_debug: print(f"[{self.agent_name}] 구조화 중 오류: {e}") raise e async def write_sections(self, state: ReportWritingState, config: RunnableConfig) -> ReportWritingState: """섹션별 내용 작성""" try: state.sections = [] for i, outline_item in enumerate(state.report_outline): # 각 섹션에 대한 내용 생성 section_prompt = f"""다음 섹션에 대한 내용을 작성하세요: 섹션: {outline_item} 주제: {state.topic} 참고 데이터: {state.research_data[:1500]}... 다음 사항을 고려하세요: 1. 명확하고 간결한 문장 사용 2. 사실과 데이터 기반의 내용 3. 논리적 흐름 유지 4. 전문적인 어조 200-500단어로 작성하세요.""" response = await self.model.ainvoke([HumanMessage(content=section_prompt)], config) # 섹션 제목 추출 (개요에서 번호와 제목 분리) title_parts = outline_item.split('.', 1) title = title_parts[1].strip() if len(title_parts) > 1 else outline_item section = ReportSection( title=title, content=response.content, order=i ) state.sections.append(section) if self.is_debug: print(f"[{self.agent_name}] 섹션 작성 완료: {title}") return state except Exception as e: if self.is_debug: print(f"[{self.agent_name}] 섹션 작성 중 오류: {e}") raise e async def compile_draft(self, state: ReportWritingState, config: RunnableConfig) -> ReportWritingState: """초안 편집""" try: # 마크다운 형식으로 보고서 편집 draft_parts = [ f"# {state.topic}", f"\n**작성일**: {state.report_metadata['created_at'][:10]}", f"**작성자**: {state.report_metadata['author']}", "\n---\n" ] # 섹션들을 순서대로 추가 for section in sorted(state.sections, key=lambda x: x.order): # 섹션 제목 레벨 결정 if "요약" in section.title or "Executive" in section.title: draft_parts.append(f"\n## {section.title}\n") elif any(keyword in section.title for keyword in ["서론", "본론", "결론", "권고"]): draft_parts.append(f"\n## {section.title}\n") else: draft_parts.append(f"\n### {section.title}\n") draft_parts.append(section.content) # 인용 추가 (있는 경우) if state.citations: draft_parts.append("\n## 참고문헌\n") for i, citation in enumerate(state.citations): draft_parts.append(f"{i+1}. {citation}") state.draft_report = "\n".join(draft_parts) if self.is_debug: print(f"[{self.agent_name}] 초안 편집 완료:") print(f" 초안 길이: {len(state.draft_report)} 문자") return state except Exception as e: if self.is_debug: print(f"[{self.agent_name}] 초안 편집 중 오류: {e}") raise e async def review_quality(self, state: ReportWritingState, config: RunnableConfig) -> ReportWritingState: """품질 검토""" try: review_prompt = f"""다음 보고서 초안의 품질을 검토하고 평가하세요: 보고서 초안: {state.draft_report[:3000]}... 다음 기준으로 평가하세요: 1. 구조와 논리적 흐름 (20점) 2. 내용의 완성도와 정확성 (30점) 3. 가독성과 명확성 (20점) 4. 전문성과 어조 (15점) 5. 인용과 참고문헌 (15점) 총 100점 만점으로 점수를 매기고, 80점 이상이면 "승인", 그 미만이면 "수정필요"로 판단하세요. 개선이 필요한 부분을 구체적으로 지적하세요. 형식: 점수: XX/100 판정: 승인/수정필요 피드백: ...""" response = await self.model.ainvoke([HumanMessage(content=review_prompt)], config) # 점수 추출 import re score_match = re.search(r'점수[:\s]*(\d+)', response.content) if score_match: state.quality_score = float(score_match.group(1)) / 100.0 # 수정 필요 여부 판단 if "수정필요" in response.content or state.quality_score < 0.8: state.needs_revision = True # 피드백을 연구 데이터에 추가하여 다음 작성에 반영 state.research_data += f"\n\n품질 검토 피드백:\n{response.content}" else: state.needs_revision = False if self.is_debug: print(f"[{self.agent_name}] 품질 검토 완료:") print(f" 품질 점수: {state.quality_score * 100:.1f}/100") print(f" 수정 필요: {state.needs_revision}") return state except Exception as e: if self.is_debug: print(f"[{self.agent_name}] 품질 검토 중 오류: {e}") raise e async def finalize_report(self, state: ReportWritingState, config: RunnableConfig) -> ReportWritingState: """최종 보고서 완성""" try: # 최종 다듬기 finalize_prompt = f"""다음 보고서를 최종적으로 다듬어주세요: {state.draft_report} 다음 사항을 확인하고 개선하세요: 1. 오탈자나 문법 오류 수정 2. 일관된 형식과 스타일 유지 3. 전환 문구 추가로 흐름 개선 4. 핵심 메시지 강조 최종 보고서를 완성된 형태로 반환하세요.""" response = await self.model.ainvoke([HumanMessage(content=finalize_prompt)], config) state.final_report = response.content # 메타데이터 업데이트 state.report_metadata["finalized_at"] = datetime.now().isoformat() state.report_metadata["quality_score"] = state.quality_score # 최종 메시지 추가 state.messages.append( AIMessage(content=f"보고서 작성이 완료되었습니다. (품질 점수: {state.quality_score * 100:.1f}/100)") ) if self.is_debug: print(f"[{self.agent_name}] 보고서 완성:") print(f" 최종 보고서 길이: {len(state.final_report)} 문자") print(f" 섹션 수: {len(state.sections)}") return state except Exception as e: if self.is_debug: print(f"[{self.agent_name}] 보고서 완성 중 오류: {e}") raise e def should_revise(self, state: ReportWritingState) -> Literal["revise", "finalize"]: """수정 필요 여부에 따라 다음 단계 결정""" return "revise" if state.needs_revision else "finalize"

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/HyunjunJeon/vibecoding-lg-mcp-a2a'

If you have feedback or need assistance with the MCP directory API, please join our Discord server