[add] analysis report error solved

This commit is contained in:
kimheesu 2025-07-07 16:12:32 +09:00
parent ab1b0120c2
commit eb5a95c74b
9 changed files with 179 additions and 45 deletions

View File

@ -17,6 +17,7 @@ from tradingagents.default_config import DEFAULT_CONFIG
from analysis.application.websocket_manager import WebSocketManager from analysis.application.websocket_manager import WebSocketManager
from analysis.infra.db_models.analysis import AnalysisStatus from analysis.infra.db_models.analysis import AnalysisStatus
# 로거 설정 - 모듈명을 명확히 지정
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class AnalysisService: class AnalysisService:
@ -31,6 +32,9 @@ class AnalysisService:
self.session = session self.session = session
self.ulid = ulid self.ulid = ulid
self.websocket_manager = websocket_manager self.websocket_manager = websocket_manager
# 서비스 초기화 로그
logger.info("🎯 AnalysisService 초기화 완료")
def get_analysis_list( def get_analysis_list(
self, self,
@ -107,16 +111,19 @@ class AnalysisService:
async def _run_analysis(self, analysis_id: str): async def _run_analysis(self, analysis_id: str):
"""백그라운드에서 실제 분석을 실행하는 메서드""" """백그라운드에서 실제 분석을 실행하는 메서드"""
try: try:
logger.info(f"🔄 분석 시작 - Analysis ID: {analysis_id}")
logger.info(f"🔍 analysis_id type: {type(analysis_id)}, value: {repr(analysis_id)}")
analysis = AnalysisVO( analysis = AnalysisVO(
id=analysis_id, id=analysis_id,
status=AnalysisStatus.RUNNING, status=AnalysisStatus.RUNNING,
updated_at=datetime.now() updated_at=datetime.now()
) )
logger.info(f"🔍 Created AnalysisVO.id: {analysis.id}, type: {type(analysis.id)}")
analysis = self.analysis_repo.update(analysis) analysis = self.analysis_repo.update(analysis)
if not analysis: if not analysis:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Analysis not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Analysis not found")
await self.websocket_manager.send_analysis_update( await self.websocket_manager.send_analysis_update(
analysis_id=analysis_id, analysis_id=analysis_id,
update_type="status_changed", update_type="status_changed",
@ -125,13 +132,14 @@ class AnalysisService:
# TradingAgentsGraph 설정 및 실행 # TradingAgentsGraph 설정 및 실행
if analysis: if analysis:
config = self._create_config(analysis) config = self._create_config(analysis)
# 분석 실행 (실제 구현) # 분석 실행 (실제 구현)
await self._execute_trading_analysis(analysis_id, analysis, config) await self._execute_trading_analysis(analysis_id, analysis, config)
logger.info(f"🔄 분석 완료 - Analysis ID: {analysis_id}")
# 완료 상태로 업데이트 # 완료 상태로 업데이트
completed_analysis = AnalysisVO( completed_analysis = AnalysisVO(
id=analysis_id, id=analysis_id,
@ -144,8 +152,10 @@ class AnalysisService:
except Exception as e: except Exception as e:
logger.error(f"🔴 분석 실패 - Analysis ID: {analysis_id}, 오류: {str(e)}")
now = datetime.now() now = datetime.now()
updates = AnalysisVO( updates = AnalysisVO(
id=analysis_id,
status=AnalysisStatus.FAILED, status=AnalysisStatus.FAILED,
error_message=str(e), error_message=str(e),
completed_at = now, completed_at = now,
@ -158,7 +168,7 @@ class AnalysisService:
def _create_config(self, analysis: AnalysisVO) -> dict: def _create_config(self, analysis: AnalysisVO) -> dict:
"""분석 설정을 생성하는 메서드""" """분석 설정을 생성하는 메서드"""
config = {} config = DEFAULT_CONFIG.copy()
config.update({ config.update({
"max_debate_rounds": analysis.research_depth, "max_debate_rounds": analysis.research_depth,
"max_risk_discuss_rounds": analysis.research_depth, "max_risk_discuss_rounds": analysis.research_depth,
@ -172,9 +182,9 @@ class AnalysisService:
async def _execute_trading_analysis(self, analysis_id: str, analysis: AnalysisVO, config: dict): async def _execute_trading_analysis(self, analysis_id: str, analysis: AnalysisVO, config: dict):
"""실제 TradingAgentsGraph를 실행하는 메서드""" """실제 TradingAgentsGraph를 실행하는 메서드"""
try: try:
logger.info(f"Starting trading analysis for {analysis_id} with ticker {analysis.ticker}") logger.info(f"📊 거래 분석 시작 - ID: {analysis_id}, 티커: {analysis.ticker}")
logger.info(f"Analysts selected: {analysis.analysts_selected}") logger.info(f"👥 선택된 분석가들: {analysis.analysts_selected}")
logger.info(f"Config: {config}") logger.info(f"⚙️ 설정: {config}")
# TradingAgentsGraph 초기화 # TradingAgentsGraph 초기화
graph = TradingAgentsGraph( graph = TradingAgentsGraph(
@ -182,7 +192,7 @@ class AnalysisService:
config=config, config=config,
debug=True debug=True
) )
logger.info("TradingAgentsGraph initialized successfully") logger.info("✅ TradingAgentsGraph 초기화 완료")
# 초기 상태 생성 # 초기 상태 생성
init_agent_state = graph.propagator.create_initial_state( init_agent_state = graph.propagator.create_initial_state(
@ -192,12 +202,12 @@ class AnalysisService:
args = graph.propagator.get_graph_args() args = graph.propagator.get_graph_args()
# 분석 실행 및 결과 처리 # 분석 실행 및 결과 처리
logger.info("Starting graph execution...") logger.info("🚀 그래프 실행 시작...")
trace = [] trace = []
chunk_count = 0 chunk_count = 0
async for chunk in graph.graph.astream(init_agent_state, **args): async for chunk in graph.graph.astream(init_agent_state, **args):
chunk_count += 1 chunk_count += 1
logger.info(f"Processing chunk {chunk_count}: {list(chunk.keys()) if chunk else 'Empty chunk'}") logger.info(f"📦 청크 처리 중 {chunk_count}: {list(chunk.keys()) if chunk else '빈 청크'}")
trace.append(chunk) trace.append(chunk)
# 실시간으로 분석 결과 업데이트 # 실시간으로 분석 결과 업데이트
@ -222,8 +232,11 @@ class AnalysisService:
self.analysis_repo.update(updates) self.analysis_repo.update(updates)
self.session.commit() self.session.commit()
logger.info(f"🎉 분석 완료 - ID: {analysis_id}")
except Exception as e: except Exception as e:
logger.error(f"🔴 분석 실패 - Analysis ID: {analysis_id}, 오류: {str(e)}")
raise Exception(f"Analysis execution failed: {str(e)}") raise Exception(f"Analysis execution failed: {str(e)}")
async def _process_analysis_chunk(self, analysis_id: str, chunk: dict): async def _process_analysis_chunk(self, analysis_id: str, chunk: dict):

View File

@ -4,16 +4,16 @@ from typing import List, Dict, Union
from analysis.infra.db_models.analysis import AnalysisStatus from analysis.infra.db_models.analysis import AnalysisStatus
class Analysis(BaseModel): class Analysis(BaseModel):
id: str id: str | None = None
member_id: str | None = None member_id: str | None = None
ticker: str | None = None ticker: str | None = None
analysis_date: date | None = None analysis_date: date | None = None
analysts_selected: list[str] = [] analysts_selected: list[str] = []
research_depth: int = 3 research_depth: int = 1
llm_provider: str = "openai" llm_provider: str = "google"
backend_url: str = "https://api.openai.com/v1" backend_url: str = "https://generativelanguage.googleapis.com/v1"
shallow_thinker: str = "gpt-4o" shallow_thinker: str = "gemini-2.5-flash-lite-preview-06-17"
deep_thinker: str = "o3" deep_thinker: str = "gemini-2.5-flash-lite-preview-06-17"
status: AnalysisStatus = AnalysisStatus.PENDING status: AnalysisStatus = AnalysisStatus.PENDING
# 개별 분석가 리포트들 # 개별 분석가 리포트들

View File

@ -3,6 +3,7 @@ from typing import TYPE_CHECKING
from sqlmodel import SQLModel, Field, JSON, Relationship from sqlmodel import SQLModel, Field, JSON, Relationship
import enum import enum
from sqlalchemy import Column, Text from sqlalchemy import Column, Text
from sqlalchemy.dialects.mysql import LONGTEXT
# TYPE_CHECKING을 사용해서 circular import 방지 # TYPE_CHECKING을 사용해서 circular import 방지
if TYPE_CHECKING: if TYPE_CHECKING:
@ -32,22 +33,22 @@ class Analysis(SQLModel, table=True):
status: AnalysisStatus = Field(default=AnalysisStatus.PENDING) status: AnalysisStatus = Field(default=AnalysisStatus.PENDING)
# 개별 분석가 리포트들 # 개별 분석가 리포트들
market_report: str | None = Field(default=None, sa_column=Column(Text), description="Market Analyst 리포트") market_report: str | None = Field(default=None, sa_column=Column(LONGTEXT), description="Market Analyst 리포트")
sentiment_report: str | None = Field(default=None, sa_column=Column(Text), description="Social Analyst 리포트") sentiment_report: str | None = Field(default=None, sa_column=Column(LONGTEXT), description="Social Analyst 리포트")
news_report: str | None = Field(default=None, sa_column=Column(Text), description="News Analyst 리포트") news_report: str | None = Field(default=None, sa_column=Column(LONGTEXT), description="News Analyst 리포트")
fundamentals_report: str | None = Field(default=None, sa_column=Column(Text), description="Fundamentals Analyst 리포트") fundamentals_report: str | None = Field(default=None, sa_column=Column(LONGTEXT), description="Fundamentals Analyst 리포트")
# 팀별 의사결정 과정 # 팀별 의사결정 과정
investment_debate_state: dict | None = Field(default=None, sa_column=Column(JSON), description="Research Team 토론 과정") investment_debate_state: dict | None = Field(default=None, sa_column=Column(JSON), description="Research Team 토론 과정")
trader_investment_plan: str | None = Field(default=None, sa_column=Column(Text), description="Trading Team 계획") trader_investment_plan: str | None = Field(default=None, sa_column=Column(LONGTEXT), description="Trading Team 계획")
risk_debate_state: dict | None = Field(default=None, sa_column=Column(JSON), description="Risk Management Team 토론 과정") risk_debate_state: dict | None = Field(default=None, sa_column=Column(JSON), description="Risk Management Team 토론 과정")
# 최종 결과물 # 최종 결과물
final_trade_decision: str | None = Field(default=None, sa_column=Column(Text), description="최종 거래 결정") final_trade_decision: str | None = Field(default=None, sa_column=Column(LONGTEXT), description="최종 거래 결정")
final_report: str | None = Field(default=None, sa_column=Column(Text), description="전체 통합 리포트") final_report: str | None = Field(default=None, sa_column=Column(LONGTEXT), description="전체 통합 리포트")
# 실행 결과 정보 # 실행 결과 정보
error_message: str | None = Field(default=None, sa_column=Column(Text)) error_message: str | None = Field(default=None, sa_column=Column(LONGTEXT))
completed_at: datetime | None = None completed_at: datetime | None = None
created_at : datetime = Field(nullable=False) created_at : datetime = Field(nullable=False)
updated_at : datetime = Field(nullable=False) updated_at : datetime = Field(nullable=False)

View File

@ -6,13 +6,16 @@ from analysis.interface.dto import TradingAnalysisRequest
from utils.db_utils import row_to_dict from utils.db_utils import row_to_dict
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from datetime import datetime, date from datetime import datetime, date
import logging
logger = logging.getLogger(__name__)
class AnalysisRepository(IAnalysisRepository): class AnalysisRepository(IAnalysisRepository):
def __init__(self, session: Session): def __init__(self, session: Session):
self.session = session self.session = session
def find_by_member_id(self, member_id: str) -> list[AnalysisVO] | None: def find_by_member_id(self, member_id: str) -> list[AnalysisVO] | None:
query = select(Analysis).where(Analysis.member_id == member_id) query = select(Analysis).where(Analysis.member_id == member_id).order_by(Analysis.created_at.desc())
analyses = self.session.exec(query).all() analyses = self.session.exec(query).all()
if not analyses: if not analyses:
@ -33,13 +36,14 @@ class AnalysisRepository(IAnalysisRepository):
self.session.add(new_analysis) self.session.add(new_analysis)
self.session.flush() self.session.flush()
self.session.refresh(new_analysis)
analysis.id = new_analysis.id analysis.id = new_analysis.id
return analysis return analysis
def update(self, analysis_vo: AnalysisVO) -> AnalysisVO | None: def update(self, analysis_vo: AnalysisVO) -> AnalysisVO | None:
analysis = self.session.get(Analysis, analysis_vo.id) analysis = self.session.get(Analysis, analysis_vo.id)
logger.info(f"🔄 분석 업데이트 - Analysis ID: {analysis_vo.id}")
if not analysis: if not analysis:
return None return None

View File

@ -27,7 +27,9 @@ def get_analysis_list_for_member(
AnalysisSessionResponse( AnalysisSessionResponse(
id=analysis.id, id=analysis.id,
ticker=analysis.ticker, ticker=analysis.ticker,
status=analysis.status status=analysis.status,
shallow_thinker=analysis.shallow_thinker,
deep_thinker=analysis.deep_thinker
) for analysis in analyses ) for analysis in analyses
] ]
@ -46,9 +48,7 @@ def start_analysis_session(
try: try:
new_analysis = analysis_service.create_analysis(current_member.id, request, background_tasks) new_analysis = analysis_service.create_analysis(current_member.id, request, background_tasks)
return AnalysisSessionResponse( return AnalysisSessionResponse(
id=new_analysis.id, **new_analysis.model_dump()
ticker=new_analysis.ticker,
status=new_analysis.status
) )
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(

View File

@ -14,16 +14,18 @@ class TradingAnalysisRequest(BaseModel):
ticker: str = "NVDA" ticker: str = "NVDA"
analysis_date: str = "2025-07-07" analysis_date: str = "2025-07-07"
analysts: List[AnalystType] = [AnalystType.MARKET, AnalystType.SOCIAL, AnalystType.NEWS, AnalystType.FUNDAMENTALS] analysts: List[AnalystType] = [AnalystType.MARKET, AnalystType.SOCIAL, AnalystType.NEWS, AnalystType.FUNDAMENTALS]
research_depth: int = 3 research_depth: int = 1
llm_provider: str = "openai" llm_provider: str = "google"
backend_url: str = "https://api.openai.com/v1" backend_url: str = "https://generativelanguage.googleapis.com/v1"
shallow_thinker: str = "gpt-4o-mini" shallow_thinker: str = "gemini-2.5-flash-lite-preview-06-17"
deep_thinker: str = "gpt-4o-mini" deep_thinker: str = "gemini-2.5-flash-lite-preview-06-17"
class AnalysisSessionResponse(BaseModel): class AnalysisSessionResponse(BaseModel):
id : str id : str
ticker : str ticker : str
status : AnalysisStatus status : AnalysisStatus
shallow_thinker : str
deep_thinker : str
class AnalysisProgressUpdate(BaseModel): class AnalysisProgressUpdate(BaseModel):
analysis_id: str analysis_id: str

View File

@ -2,19 +2,12 @@ from fastapi import FastAPI
from utils.database import create_db_and_tables from utils.database import create_db_and_tables
from utils.containers import Container from utils.containers import Container
from analysis.interface.controller.analysis_controller import router as analysis_router from analysis.interface.controller.analysis_controller import router as analysis_router
from member.interface.controller.member_controller import router as member_router from member.interface.controller.member_controller import router as member_router
import logging import logging
from utils.logger import setup_logging
# 로깅 설정 setup_logging()
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(), # 콘솔 출력
]
)
@ -24,7 +17,15 @@ app.container = Container()
app.include_router(analysis_router) app.include_router(analysis_router)
app.include_router(member_router) app.include_router(member_router)
@app.on_event("startup") @app.on_event("startup")
def startup_db_client(): def startup_db_client():
create_db_and_tables() logger = logging.getLogger(__name__)
logger.info("🚀 FastAPI 애플리케이션 시작")
create_db_and_tables()
logger.info("📊 데이터베이스 초기화 완료")
@app.get("/")
def root():
logger = logging.getLogger(__name__)
logger.info("📍 루트 엔드포인트 호출됨")
return {"message": "Trading Agents API"}

View File

@ -11,6 +11,7 @@ click-didyoumean==0.3.1
click-plugins==1.1.1.2 click-plugins==1.1.1.2
click-repl==0.3.0 click-repl==0.3.0
colorama==0.4.6 colorama==0.4.6
colorlog==6.9.0
cryptography==45.0.4 cryptography==45.0.4
dependency-injector==4.48.1 dependency-injector==4.48.1
dnspython==2.7.0 dnspython==2.7.0

112
backend/utils/logger.py Normal file
View File

@ -0,0 +1,112 @@
import sys
import logging
import colorlog
from typing import Optional
# 강화된 컬러 로깅 설정
def setup_logging():
# 루트 로거 설정
root_logger = logging.getLogger()
root_logger.setLevel(logging.INFO)
# 기존 핸들러 제거 (중복 방지)
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
# 컬러 콘솔 핸들러 생성
console_handler = colorlog.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
# 컬러 포맷터 설정
color_formatter = colorlog.ColoredFormatter(
"%(log_color)s%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt='%Y-%m-%d %H:%M:%S',
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red,bg_white',
},
secondary_log_colors={},
style='%'
)
console_handler.setFormatter(color_formatter)
# 핸들러 추가
root_logger.addHandler(console_handler)
# FastAPI/uvicorn 로거 설정
logging.getLogger("uvicorn").setLevel(logging.INFO)
logging.getLogger("uvicorn.access").setLevel(logging.INFO)
logging.getLogger("fastapi").setLevel(logging.INFO)
# 애플리케이션 로거들 설정
logging.getLogger("analysis").setLevel(logging.INFO)
logging.getLogger("analysis.application").setLevel(logging.INFO)
logging.getLogger("analysis.application.analysis_service").setLevel(logging.INFO)
# 로깅 설정 완료 메시지
logger = logging.getLogger(__name__)
logger.info("✅ 컬러 로깅 설정 완료")
def get_colored_logger(
name: str,
level: int = logging.INFO,
format_string: Optional[str] = None
) -> logging.Logger:
"""
컬러 로거를 가져오는 함수
Args:
name: 로거 이름
level: 로깅 레벨
format_string: 커스텀 포맷 문자열
Returns:
설정된 컬러 로거
"""
# 기본 포맷 설정
if format_string is None:
format_string = (
"%(log_color)s%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
# 컬러 포맷터 생성
color_formatter = colorlog.ColoredFormatter(
format_string,
datefmt='%Y-%m-%d %H:%M:%S',
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red,bg_white',
},
secondary_log_colors={},
style='%'
)
# 로거 생성
logger = logging.getLogger(name)
logger.setLevel(level)
# 이미 핸들러가 있으면 기존 로거 반환
if logger.handlers:
return logger
# 콘솔 핸들러 생성
console_handler = colorlog.StreamHandler()
console_handler.setFormatter(color_formatter)
console_handler.setLevel(level)
# 핸들러 추가
logger.addHandler(console_handler)
# 상위 로거로 전파 방지
logger.propagate = False
return logger