diff --git a/backend/analysis/application/analysis_service.py b/backend/analysis/application/analysis_service.py index 8afa0c37..98d64b36 100644 --- a/backend/analysis/application/analysis_service.py +++ b/backend/analysis/application/analysis_service.py @@ -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): diff --git a/backend/analysis/domain/analysis.py b/backend/analysis/domain/analysis.py index 64b991f2..e15f3c04 100644 --- a/backend/analysis/domain/analysis.py +++ b/backend/analysis/domain/analysis.py @@ -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 # 개별 분석가 리포트들 diff --git a/backend/analysis/infra/db_models/analysis.py b/backend/analysis/infra/db_models/analysis.py index bc0b55df..fdb93905 100644 --- a/backend/analysis/infra/db_models/analysis.py +++ b/backend/analysis/infra/db_models/analysis.py @@ -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) diff --git a/backend/analysis/infra/repository/analysis_repo.py b/backend/analysis/infra/repository/analysis_repo.py index e41670b6..96bb89f8 100644 --- a/backend/analysis/infra/repository/analysis_repo.py +++ b/backend/analysis/infra/repository/analysis_repo.py @@ -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 diff --git a/backend/analysis/interface/controller/analysis_controller.py b/backend/analysis/interface/controller/analysis_controller.py index 8ea16f14..9814559b 100644 --- a/backend/analysis/interface/controller/analysis_controller.py +++ b/backend/analysis/interface/controller/analysis_controller.py @@ -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( diff --git a/backend/analysis/interface/dto.py b/backend/analysis/interface/dto.py index 0d61dcc5..4905d7af 100644 --- a/backend/analysis/interface/dto.py +++ b/backend/analysis/interface/dto.py @@ -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 diff --git a/backend/main.py b/backend/main.py index c31a6fdf..3eddbf65 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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() \ No newline at end of file + 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"} \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 875b85dd..034ef237 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -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 diff --git a/backend/utils/logger.py b/backend/utils/logger.py new file mode 100644 index 00000000..a0df9304 --- /dev/null +++ b/backend/utils/logger.py @@ -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 \ No newline at end of file