[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.infra.db_models.analysis import AnalysisStatus
# 로거 설정 - 모듈명을 명확히 지정
logger = logging.getLogger(__name__)
class AnalysisService:
@ -31,6 +32,9 @@ class AnalysisService:
self.session = session
self.ulid = ulid
self.websocket_manager = websocket_manager
# 서비스 초기화 로그
logger.info("🎯 AnalysisService 초기화 완료")
def get_analysis_list(
self,
@ -107,16 +111,19 @@ class AnalysisService:
async def _run_analysis(self, analysis_id: str):
"""백그라운드에서 실제 분석을 실행하는 메서드"""
try:
logger.info(f"🔄 분석 시작 - Analysis ID: {analysis_id}")
logger.info(f"🔍 analysis_id type: {type(analysis_id)}, value: {repr(analysis_id)}")
analysis = AnalysisVO(
id=analysis_id,
status=AnalysisStatus.RUNNING,
updated_at=datetime.now()
)
logger.info(f"🔍 Created AnalysisVO.id: {analysis.id}, type: {type(analysis.id)}")
analysis = self.analysis_repo.update(analysis)
if not analysis:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Analysis not found")
await self.websocket_manager.send_analysis_update(
analysis_id=analysis_id,
update_type="status_changed",
@ -125,13 +132,14 @@ class AnalysisService:
# TradingAgentsGraph 설정 및 실행
if analysis:
config = self._create_config(analysis)
# 분석 실행 (실제 구현)
await self._execute_trading_analysis(analysis_id, analysis, config)
logger.info(f"🔄 분석 완료 - Analysis ID: {analysis_id}")
# 완료 상태로 업데이트
completed_analysis = AnalysisVO(
id=analysis_id,
@ -144,8 +152,10 @@ class AnalysisService:
except Exception as e:
logger.error(f"🔴 분석 실패 - Analysis ID: {analysis_id}, 오류: {str(e)}")
now = datetime.now()
updates = AnalysisVO(
id=analysis_id,
status=AnalysisStatus.FAILED,
error_message=str(e),
completed_at = now,
@ -158,7 +168,7 @@ class AnalysisService:
def _create_config(self, analysis: AnalysisVO) -> dict:
"""분석 설정을 생성하는 메서드"""
config = {}
config = DEFAULT_CONFIG.copy()
config.update({
"max_debate_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):
"""실제 TradingAgentsGraph를 실행하는 메서드"""
try:
logger.info(f"Starting trading analysis for {analysis_id} with ticker {analysis.ticker}")
logger.info(f"Analysts selected: {analysis.analysts_selected}")
logger.info(f"Config: {config}")
logger.info(f"📊 거래 분석 시작 - ID: {analysis_id}, 티커: {analysis.ticker}")
logger.info(f"👥 선택된 분석가들: {analysis.analysts_selected}")
logger.info(f"⚙️ 설정: {config}")
# TradingAgentsGraph 초기화
graph = TradingAgentsGraph(
@ -182,7 +192,7 @@ class AnalysisService:
config=config,
debug=True
)
logger.info("TradingAgentsGraph initialized successfully")
logger.info("✅ TradingAgentsGraph 초기화 완료")
# 초기 상태 생성
init_agent_state = graph.propagator.create_initial_state(
@ -192,12 +202,12 @@ class AnalysisService:
args = graph.propagator.get_graph_args()
# 분석 실행 및 결과 처리
logger.info("Starting graph execution...")
logger.info("🚀 그래프 실행 시작...")
trace = []
chunk_count = 0
async for chunk in graph.graph.astream(init_agent_state, **args):
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)
# 실시간으로 분석 결과 업데이트
@ -222,8 +232,11 @@ class AnalysisService:
self.analysis_repo.update(updates)
self.session.commit()
logger.info(f"🎉 분석 완료 - ID: {analysis_id}")
except Exception as e:
logger.error(f"🔴 분석 실패 - Analysis ID: {analysis_id}, 오류: {str(e)}")
raise Exception(f"Analysis execution failed: {str(e)}")
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
class Analysis(BaseModel):
id: str
id: str | None = None
member_id: str | None = None
ticker: str | None = None
analysis_date: date | None = None
analysts_selected: list[str] = []
research_depth: int = 3
llm_provider: str = "openai"
backend_url: str = "https://api.openai.com/v1"
shallow_thinker: str = "gpt-4o"
deep_thinker: str = "o3"
research_depth: int = 1
llm_provider: str = "google"
backend_url: str = "https://generativelanguage.googleapis.com/v1"
shallow_thinker: str = "gemini-2.5-flash-lite-preview-06-17"
deep_thinker: str = "gemini-2.5-flash-lite-preview-06-17"
status: AnalysisStatus = AnalysisStatus.PENDING
# 개별 분석가 리포트들

View File

@ -3,6 +3,7 @@ from typing import TYPE_CHECKING
from sqlmodel import SQLModel, Field, JSON, Relationship
import enum
from sqlalchemy import Column, Text
from sqlalchemy.dialects.mysql import LONGTEXT
# TYPE_CHECKING을 사용해서 circular import 방지
if TYPE_CHECKING:
@ -32,22 +33,22 @@ class Analysis(SQLModel, table=True):
status: AnalysisStatus = Field(default=AnalysisStatus.PENDING)
# 개별 분석가 리포트들
market_report: str | None = Field(default=None, sa_column=Column(Text), description="Market Analyst 리포트")
sentiment_report: str | None = Field(default=None, sa_column=Column(Text), description="Social Analyst 리포트")
news_report: str | None = Field(default=None, sa_column=Column(Text), description="News Analyst 리포트")
fundamentals_report: str | None = Field(default=None, sa_column=Column(Text), description="Fundamentals 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(LONGTEXT), description="Social 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(LONGTEXT), description="Fundamentals Analyst 리포트")
# 팀별 의사결정 과정
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 토론 과정")
# 최종 결과물
final_trade_decision: str | None = Field(default=None, sa_column=Column(Text), description="최종 거래 결정")
final_report: 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(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
created_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 sqlalchemy.orm import selectinload
from datetime import datetime, date
import logging
logger = logging.getLogger(__name__)
class AnalysisRepository(IAnalysisRepository):
def __init__(self, session: Session):
self.session = session
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()
if not analyses:
@ -33,13 +36,14 @@ class AnalysisRepository(IAnalysisRepository):
self.session.add(new_analysis)
self.session.flush()
self.session.refresh(new_analysis)
analysis.id = new_analysis.id
return analysis
def update(self, analysis_vo: AnalysisVO) -> AnalysisVO | None:
analysis = self.session.get(Analysis, analysis_vo.id)
logger.info(f"🔄 분석 업데이트 - Analysis ID: {analysis_vo.id}")
if not analysis:
return None

View File

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

View File

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

View File

@ -2,19 +2,12 @@ from fastapi import FastAPI
from utils.database import create_db_and_tables
from utils.containers import Container
from analysis.interface.controller.analysis_controller import router as analysis_router
from member.interface.controller.member_controller import router as member_router
import logging
from utils.logger import setup_logging
# 로깅 설정
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(), # 콘솔 출력
]
)
setup_logging()
@ -24,7 +17,15 @@ app.container = Container()
app.include_router(analysis_router)
app.include_router(member_router)
@app.on_event("startup")
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-repl==0.3.0
colorama==0.4.6
colorlog==6.9.0
cryptography==45.0.4
dependency-injector==4.48.1
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