Files
deepagent/scripts/run_ai_trend_research.py
HyunjunJeon 6f01c834ba feat: Deep Research Agent 확장 - Ralph Loop, 깊이 설정, 테스트 스위트 추가
- research_agent/tools.py: 한글 Docstring 및 ASCII 흐름도 추가
- research_agent/researcher/depth.py: ResearchDepth enum 및 DepthConfig 추가
- research_agent/researcher/ralph_loop.py: Ralph Loop 반복 연구 패턴 구현
- research_agent/researcher/runner.py: 연구 실행기 (CLI 지원)
- tests/researcher/: 91개 테스트 (실제 API 호출 포함)
- scripts/run_ai_trend_research.py: AI 트렌드 연구 스크립트 + 도구 궤적 로깅
2026-01-12 15:49:43 +09:00

749 lines
24 KiB
Python

#!/usr/bin/env python3
"""2026 AI 트렌드 키워드 연구 스크립트 (도구 궤적 로깅 포함).
이 스크립트는 다양한 소스에서 2026년 AI 트렌드를 조사하고 보고서를 생성합니다.
각 도구 호출은 TOOL_TRAJECTORY.log 및 TOOL_TRAJECTORY.json에 기록됩니다.
## 스크립트 실행 흐름
```
┌─────────────────────────────────────────────────────────────────┐
│ main() │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 세션 초기화 │
│ session = ResearchSession(query, session_id) │
│ trajectory_logger = ToolTrajectoryLogger(session_dir) │
│ │
│ 2. 다중 소스 검색 │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ search_web_sources() → tavily_search (5회) ││
│ │ search_github_sources() → github_code_search (3회) ││
│ │ search_arxiv_sources() → arxiv_search (3회) ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ 3. 키워드 분석 │
│ keywords = extract_keywords(findings) │
│ │
│ 4. 결과 저장 │
│ - AI_TREND_REPORT.md │
│ - TOOL_TRAJECTORY.log │
│ - TOOL_TRAJECTORY.json │
│ - SUMMARY.md │
│ │
└─────────────────────────────────────────────────────────────────┘
```
사용법:
uv run python scripts/run_ai_trend_research.py
"""
from __future__ import annotations
import json
from dataclasses import asdict, dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any
from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn
from rich.table import Table
from research_agent.researcher.ralph_loop import (
Finding,
ResearchSession,
SourceQuality,
SourceType,
)
from research_agent.tools import (
arxiv_search,
github_code_search,
tavily_search,
)
# ============================================================================
# 콘솔 초기화
# ============================================================================
console = Console()
# ============================================================================
# 도구 궤적 로깅
# ============================================================================
@dataclass
class ToolCallRecord:
"""단일 도구 호출 기록을 나타내는 데이터 클래스.
Attributes:
seq: 호출 순서 번호.
tool_name: 호출된 도구 이름.
input_args: 도구에 전달된 인자 딕셔너리.
output_preview: 출력 미리보기 (최대 300자).
output_length: 전체 출력 길이.
duration_ms: 호출 소요 시간 (밀리초).
success: 성공 여부.
error: 에러 메시지 (실패 시).
timestamp: 호출 시간 (ISO 8601 형식).
"""
seq: int # 호출 순서
tool_name: str # 도구 이름
input_args: dict[str, Any] # 입력 인자
output_preview: str # 출력 미리보기
output_length: int # 출력 길이
duration_ms: float # 소요 시간 (ms)
success: bool # 성공 여부
error: str | None = None # 에러 메시지
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
class ToolTrajectoryLogger:
"""도구 호출 궤적을 로깅하는 클래스.
각 도구 호출을 기록하고, 세션 종료 시 로그 파일과 JSON 파일로 저장합니다.
Attributes:
session_dir: 로그 파일을 저장할 세션 디렉토리.
calls: 기록된 도구 호출 목록.
seq: 현재 호출 순서 번호.
"""
def __init__(self, session_dir: Path):
"""로거를 초기화한다.
Args:
session_dir: 로그 파일을 저장할 디렉토리 경로.
"""
self.session_dir = session_dir
self.calls: list[ToolCallRecord] = []
self.seq = 0
def log_call(
self,
tool_name: str,
input_args: dict[str, Any],
output: str,
duration_ms: float,
success: bool = True,
error: str | None = None,
) -> None:
"""도구 호출을 기록한다.
Args:
tool_name: 호출된 도구 이름.
input_args: 도구에 전달된 인자.
output: 도구 출력 (전체).
duration_ms: 호출 소요 시간 (밀리초).
success: 성공 여부 (기본값: True).
error: 에러 메시지 (선택).
"""
self.seq += 1
record = ToolCallRecord(
seq=self.seq,
tool_name=tool_name,
input_args=input_args,
# 출력 미리보기 (300자로 제한)
output_preview=output[:300] if len(output) > 300 else output,
output_length=len(output),
duration_ms=duration_ms,
success=success,
error=error,
)
self.calls.append(record)
def save(self) -> Path:
"""로그를 파일에 저장한다.
두 가지 형식으로 저장합니다:
- TOOL_TRAJECTORY.log: 사람이 읽을 수 있는 형식
- TOOL_TRAJECTORY.json: 프로그래밍적 분석용
Returns:
생성된 .log 파일 경로.
"""
# 텍스트 로그 파일 작성
log_path = self.session_dir / "TOOL_TRAJECTORY.log"
with open(log_path, "w") as f:
# 헤더 작성
f.write(f"Tool Trajectory Log\n")
f.write(f"Generated: {datetime.now().isoformat()}\n")
f.write(f"Total Calls: {len(self.calls)}\n")
f.write(f"Success: {sum(1 for c in self.calls if c.success)}\n")
f.write(f"Failed: {sum(1 for c in self.calls if not c.success)}\n")
f.write("=" * 70 + "\n\n")
# 각 호출 기록 작성
for call in self.calls:
status = "OK" if call.success else f"FAIL: {call.error}"
f.write(
f"[{call.seq}] {call.tool_name} ({status}) [{call.duration_ms:.0f}ms]\n"
)
f.write(f" Timestamp: {call.timestamp}\n")
f.write(
f" Args: {json.dumps(call.input_args, ensure_ascii=False)}\n"
)
f.write(f" Output Length: {call.output_length} chars\n")
f.write(f" Output Preview:\n")
# 출력 미리보기 (최대 10줄)
for line in call.output_preview.split("\n")[:10]:
f.write(f" | {line}\n")
f.write("-" * 70 + "\n\n")
# JSON 파일 작성
json_path = self.session_dir / "TOOL_TRAJECTORY.json"
with open(json_path, "w") as f:
json.dump([asdict(c) for c in self.calls], f, indent=2, ensure_ascii=False)
return log_path
# ============================================================================
# 전역 로거 (각 검색 함수에서 사용)
# ============================================================================
trajectory_logger: ToolTrajectoryLogger | None = None
# ============================================================================
# 검색 쿼리 정의
# ============================================================================
# 웹 검색 쿼리 (Tavily)
RESEARCH_QUERIES = [
"2026 AI trends predictions",
"AI agent frameworks 2026",
"context engineering LLM",
"multimodal AI applications 2026",
"AI coding assistants trends",
]
# GitHub 코드 검색 쿼리 (리터럴 코드 패턴)
GITHUB_QUERIES = [
"class Agent(", # 에이전트 클래스 정의
"def run_agent(", # 에이전트 실행 함수
"context_length =", # 컨텍스트 길이 설정
]
# arXiv 학술 검색 쿼리
ARXIV_QUERIES = [
"large language model agents",
"context window optimization",
"multimodal foundation models",
]
# ============================================================================
# 소스별 검색 함수
# ============================================================================
def search_web_sources() -> list[Finding]:
"""웹 소스에서 검색을 수행한다.
RESEARCH_QUERIES의 각 쿼리에 대해 Tavily 검색을 수행하고,
결과를 Finding 객체로 변환합니다.
Returns:
수집된 Finding 객체 목록.
"""
global trajectory_logger
findings = []
console.print("\n[bold cyan]Web Search[/bold cyan]")
for query in RESEARCH_QUERIES:
console.print(f" Searching: {query}...")
args = {"query": query, "max_results": 2, "topic": "general"}
start = datetime.now()
try:
# Tavily 검색 실행
result = tavily_search.invoke(args)
duration = (datetime.now() - start).total_seconds() * 1000
# 궤적 로깅
if trajectory_logger:
trajectory_logger.log_call("tavily_search", args, result, duration)
# 소스 품질 평가
quality = SourceQuality.from_source_type(
SourceType.WEB,
relevance_score=0.8,
recency_score=0.9,
)
# Finding 객체 생성
findings.append(
Finding(
content=result[:2000] if len(result) > 2000 else result,
source_url=f"tavily://{query}",
source_title=f"Web: {query}",
confidence=0.7,
quality=quality,
)
)
console.print(
f" [green]Found results[/green] [dim]({duration:.0f}ms)[/dim]"
)
except Exception as e:
duration = (datetime.now() - start).total_seconds() * 1000
if trajectory_logger:
trajectory_logger.log_call(
"tavily_search", args, "", duration, success=False, error=str(e)
)
console.print(f" [red]Error: {e}[/red]")
return findings
def search_github_sources() -> list[Finding]:
"""GitHub 소스에서 코드 검색을 수행한다.
GITHUB_QUERIES의 각 쿼리에 대해 grep.app API를 통한
코드 검색을 수행하고, 결과를 Finding 객체로 변환합니다.
Returns:
수집된 Finding 객체 목록.
"""
global trajectory_logger
findings = []
console.print("\n[bold cyan]GitHub Code Search[/bold cyan]")
for query in GITHUB_QUERIES:
console.print(f" Searching: {query}...")
args = {"query": query, "max_results": 5}
start = datetime.now()
try:
# GitHub 코드 검색 실행
result = github_code_search.invoke(args)
duration = (datetime.now() - start).total_seconds() * 1000
# 궤적 로깅
if trajectory_logger:
trajectory_logger.log_call("github_code_search", args, result, duration)
# 소스 품질 평가 (GitHub은 실제 구현 코드이므로 권위도 높음)
quality = SourceQuality.from_source_type(
SourceType.GITHUB,
relevance_score=0.85,
recency_score=0.7,
)
# Finding 객체 생성
findings.append(
Finding(
content=result[:2000] if len(result) > 2000 else result,
source_url=f"github://{query}",
source_title=f"GitHub: {query}",
confidence=0.75,
quality=quality,
)
)
console.print(
f" [green]Found results[/green] [dim]({duration:.0f}ms)[/dim]"
)
except Exception as e:
duration = (datetime.now() - start).total_seconds() * 1000
if trajectory_logger:
trajectory_logger.log_call(
"github_code_search",
args,
"",
duration,
success=False,
error=str(e),
)
console.print(f" [red]Error: {e}[/red]")
return findings
def search_arxiv_sources() -> list[Finding]:
"""arXiv 소스에서 학술 논문 검색을 수행한다.
ARXIV_QUERIES의 각 쿼리에 대해 arXiv API를 통한
논문 검색을 수행하고, 결과를 Finding 객체로 변환합니다.
Returns:
수집된 Finding 객체 목록.
"""
global trajectory_logger
findings = []
console.print("\n[bold cyan]arXiv Academic Search[/bold cyan]")
for query in ARXIV_QUERIES:
console.print(f" Searching: {query}...")
args = {"query": query, "max_results": 3, "sort_by": "submittedDate"}
start = datetime.now()
try:
# arXiv 검색 실행
result = arxiv_search.invoke(args)
duration = (datetime.now() - start).total_seconds() * 1000
# 궤적 로깅
if trajectory_logger:
trajectory_logger.log_call("arxiv_search", args, result, duration)
# 소스 품질 평가 (학술 논문은 가장 높은 권위도)
quality = SourceQuality.from_source_type(
SourceType.ARXIV,
relevance_score=0.9,
recency_score=0.85,
)
# Finding 객체 생성
findings.append(
Finding(
content=result[:3000] if len(result) > 3000 else result,
source_url=f"arxiv://{query}",
source_title=f"arXiv: {query}",
confidence=0.9, # 학술 소스는 높은 신뢰도
quality=quality,
)
)
console.print(
f" [green]Found results[/green] [dim]({duration:.0f}ms)[/dim]"
)
except Exception as e:
duration = (datetime.now() - start).total_seconds() * 1000
if trajectory_logger:
trajectory_logger.log_call(
"arxiv_search", args, "", duration, success=False, error=str(e)
)
console.print(f" [red]Error: {e}[/red]")
return findings
# ============================================================================
# 키워드 분석
# ============================================================================
def extract_keywords(findings: list[Finding]) -> dict[str, int]:
"""발견 항목들에서 AI 관련 키워드를 추출한다.
사전 정의된 AI 키워드 목록을 기반으로 각 키워드의
출현 빈도를 계산합니다.
Args:
findings: 분석할 Finding 객체 목록.
Returns:
키워드 -> 빈도 매핑 (빈도 내림차순 정렬).
"""
keyword_counts: dict[str, int] = {}
# AI 관련 키워드 목록
ai_keywords = [
# 에이전트 관련
"agent",
"agents",
"agentic",
# 컨텍스트 관련
"context",
"context window",
"context engineering",
# 멀티모달 관련
"multimodal",
"vision",
"audio",
# RAG 및 검색 관련
"RAG",
"retrieval",
"retrieval-augmented",
# 학습 관련
"fine-tuning",
"RLHF",
"DPO",
# 추론 관련
"reasoning",
"chain-of-thought",
"CoT",
# 코딩 관련
"code generation",
"coding assistant",
# 모델 이름
"GPT",
"Claude",
"Gemini",
"LLaMA",
"Mistral",
# 아키텍처 관련
"transformer",
"attention",
"embedding",
"vector",
"vectorstore",
# 프롬프트 관련
"prompt",
"prompting",
"prompt engineering",
# 도구 사용 관련
"tool use",
"function calling",
# 메모리 관련
"memory",
"long-term memory",
# 안전성 관련
"safety",
"alignment",
"guardrails",
# 성능 관련
"inference",
"latency",
"optimization",
# 오픈소스 관련
"open source",
"open-source",
# 평가 관련
"benchmark",
"evaluation",
# 모델 아키텍처
"MoE",
"mixture of experts",
"small language model",
"SLM",
# 엣지 AI
"on-device",
"edge AI",
]
# 각 발견 항목에서 키워드 카운트
for finding in findings:
content_lower = finding.content.lower()
for kw in ai_keywords:
if kw.lower() in content_lower:
count = content_lower.count(kw.lower())
keyword_counts[kw] = keyword_counts.get(kw, 0) + count
# 빈도 내림차순 정렬
return dict(sorted(keyword_counts.items(), key=lambda x: x[1], reverse=True))
# ============================================================================
# 보고서 생성
# ============================================================================
def generate_report(
session: ResearchSession,
keywords: dict[str, int],
output_path: Path,
) -> None:
"""연구 결과 보고서를 Markdown 형식으로 생성한다.
Args:
session: 연구 세션 객체.
keywords: 키워드 -> 빈도 매핑.
output_path: 보고서 저장 경로.
"""
# 보고서 헤더
report_content = f"""# 2026 AI 트렌드 키워드 연구 리포트
**생성일:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
**세션 ID:** {session.session_id}
**총 소스 수:** {len(session.findings)}
**Coverage Score:** {session.ralph_loop.state.coverage_score:.2%}
---
## 핵심 트렌드 키워드 (Top 20)
| 순위 | 키워드 | 빈도 |
|------|--------|------|
"""
# Top 20 키워드 테이블
for i, (kw, count) in enumerate(list(keywords.items())[:20], 1):
report_content += f"| {i} | {kw} | {count} |\n"
# 주요 발견사항 섹션
report_content += """
---
## 주요 발견사항
### 1. Agent & Agentic AI
- AI 에이전트 프레임워크가 2026년 핵심 트렌드
- 자율적 작업 수행 및 도구 사용 능력 강조
- Multi-agent 시스템의 부상
### 2. Context Engineering
- 긴 컨텍스트 윈도우 활용 최적화
- 파일시스템 기반 컨텍스트 관리
- 효율적인 정보 검색 및 주입
### 3. Multimodal AI
- 텍스트, 이미지, 오디오, 비디오 통합
- Vision-Language 모델의 발전
- 실시간 멀티모달 처리
### 4. Reasoning & CoT
- Chain-of-Thought 추론 개선
- 복잡한 문제 해결 능력 향상
- Self-reflection 및 자기 개선
### 5. Code & Development
- AI 코딩 어시스턴트의 고도화
- 전체 개발 워크플로우 자동화
- 코드 리뷰 및 디버깅 지원
---
## 소스 분석
"""
# 소스 유형별 통계
source_types = {}
for f in session.findings:
if f.quality:
st = f.quality.source_type
source_types[st] = source_types.get(st, 0) + 1
for st, count in source_types.items():
report_content += f"- **{st}**: {count}개 소스\n"
# 상세 소스 목록
report_content += f"""
---
## 상세 소스 목록
"""
for i, f in enumerate(session.findings, 1):
quality_score = f.quality.overall_score if f.quality else 0
report_content += f"""### 소스 {i}: {f.source_title}
- **신뢰도:** {f.confidence:.0%}
- **품질 점수:** {quality_score:.2f}
- **URL:** {f.source_url}
<details>
<summary>내용 미리보기</summary>
{f.content[:500]}...
</details>
---
"""
# 파일 저장
output_path.write_text(report_content)
# ============================================================================
# 메인 함수
# ============================================================================
def main() -> None:
"""스크립트 메인 함수.
1. 세션 초기화 및 로거 설정
2. 다중 소스 검색 수행
3. 키워드 분석
4. 보고서 및 로그 생성
"""
global trajectory_logger
# 시작 배너 출력
console.print(
Panel(
"[bold cyan]2026 AI Trend Keyword Research[/bold cyan]\n"
"[dim]Collecting and analyzing data from multiple sources[/dim]",
title="Research Started",
)
)
# 세션 초기화
session = ResearchSession(
query="2026 AI Trends and Keywords",
session_id=datetime.now().strftime("%Y%m%d_%H%M%S"),
)
session.initialize()
# 도구 궤적 로거 초기화
trajectory_logger = ToolTrajectoryLogger(session.session_dir)
# 세션 정보 출력
console.print(f"\n[dim]Session: {session.session_id}[/dim]")
console.print(f"[dim]Workspace: {session.session_dir}[/dim]\n")
# 프로그레스 표시와 함께 데이터 수집
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console,
) as progress:
task = progress.add_task("Collecting data...", total=None)
# 웹 소스 검색
web_findings = search_web_sources()
for f in web_findings:
session.add_finding(f)
# GitHub 소스 검색
github_findings = search_github_sources()
for f in github_findings:
session.add_finding(f)
# arXiv 소스 검색
arxiv_findings = search_arxiv_sources()
for f in arxiv_findings:
session.add_finding(f)
progress.update(task, description="Analyzing keywords...")
# 키워드 분석
keywords = extract_keywords(session.findings)
# Top 10 키워드 테이블 출력
table = Table(title="Top 10 AI 트렌드 키워드")
table.add_column("순위", style="cyan")
table.add_column("키워드", style="green")
table.add_column("빈도", style="yellow")
for i, (kw, count) in enumerate(list(keywords.items())[:10], 1):
table.add_row(str(i), kw, str(count))
console.print("\n")
console.print(table)
# 보고서 생성
report_path = session.session_dir / "AI_TREND_REPORT.md"
generate_report(session, keywords, report_path)
# 궤적 로그 저장
trajectory_log_path = trajectory_logger.save() if trajectory_logger else None
# 세션 마무리
summary_path = session.finalize()
# 완료 배너 출력
console.print(
Panel(
f"[bold green]Research Complete![/bold green]\n\n"
f"Total Sources: {len(session.findings)}\n"
f"Coverage: {session.ralph_loop.state.coverage_score:.2%}\n"
f"Keywords Found: {len(keywords)}\n"
f"Tool Calls: {len(trajectory_logger.calls) if trajectory_logger else 0}\n\n"
f"[dim]Report: {report_path}[/dim]\n"
f"[dim]Summary: {summary_path}[/dim]\n"
f"[dim]Tool Trajectory: {trajectory_log_path}[/dim]",
title="Research Complete",
border_style="green",
)
)
if __name__ == "__main__":
main()