dev/web
This commit is contained in:
parent
fbd96e9c18
commit
ab1b0120c2
|
|
@ -8,3 +8,4 @@ eval_data/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
results/
|
results/
|
||||||
.env
|
.env
|
||||||
|
tradingagents/dataflows/data_cache/
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.join(os.path.dirname(__file__), '../../..'))
|
||||||
|
|
||||||
|
import logging
|
||||||
from sqlmodel import Session
|
from sqlmodel import Session
|
||||||
from analysis.domain.repository.analysis_repo import IAnalysisRepository
|
from analysis.domain.repository.analysis_repo import IAnalysisRepository
|
||||||
from ulid import ULID
|
from ulid import ULID
|
||||||
|
|
@ -9,19 +14,23 @@ from datetime import datetime
|
||||||
|
|
||||||
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||||
from tradingagents.default_config import DEFAULT_CONFIG
|
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:
|
class AnalysisService:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
analysis_repo: IAnalysisRepository,
|
analysis_repo: IAnalysisRepository,
|
||||||
session: Session,
|
session: Session,
|
||||||
ulid: ULID
|
ulid: ULID,
|
||||||
|
websocket_manager: WebSocketManager
|
||||||
):
|
):
|
||||||
self.analysis_repo = analysis_repo
|
self.analysis_repo = analysis_repo
|
||||||
self.session = session
|
self.session = session
|
||||||
self.ulid = ulid
|
self.ulid = ulid
|
||||||
|
self.websocket_manager = websocket_manager
|
||||||
|
|
||||||
def get_analysis_list(
|
def get_analysis_list(
|
||||||
self,
|
self,
|
||||||
|
|
@ -46,6 +55,15 @@ class AnalysisService:
|
||||||
|
|
||||||
return analysis
|
return analysis
|
||||||
|
|
||||||
|
def get_analysis_sessions_by_member(
|
||||||
|
self,
|
||||||
|
member_id: str
|
||||||
|
) -> list[AnalysisVO]:
|
||||||
|
analyses = self.analysis_repo.find_by_member_id(member_id)
|
||||||
|
if not analyses:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Analysis not found")
|
||||||
|
return analyses
|
||||||
|
|
||||||
def create_analysis(
|
def create_analysis(
|
||||||
self,
|
self,
|
||||||
member_id: str,
|
member_id: str,
|
||||||
|
|
@ -67,14 +85,20 @@ class AnalysisService:
|
||||||
backend_url=request.backend_url,
|
backend_url=request.backend_url,
|
||||||
shallow_thinker=request.shallow_thinker,
|
shallow_thinker=request.shallow_thinker,
|
||||||
deep_thinker=request.deep_thinker,
|
deep_thinker=request.deep_thinker,
|
||||||
status="pending",
|
status=AnalysisStatus.PENDING,
|
||||||
created_at=now,
|
created_at=now,
|
||||||
updated_at=now
|
updated_at=now
|
||||||
)
|
)
|
||||||
|
|
||||||
saved_analysis = self.analysis_repo.save(analysis_vo)
|
saved_analysis = self.analysis_repo.save(analysis_vo)
|
||||||
|
if not saved_analysis:
|
||||||
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to save analysis")
|
||||||
|
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
|
# Register analysis with websocket manager
|
||||||
|
self.websocket_manager.register_analysis(saved_analysis.id, member_id)
|
||||||
|
|
||||||
# 백그라운드에서 분석 실행
|
# 백그라운드에서 분석 실행
|
||||||
background_tasks.add_task(self._run_analysis, saved_analysis.id)
|
background_tasks.add_task(self._run_analysis, saved_analysis.id)
|
||||||
|
|
||||||
|
|
@ -83,48 +107,58 @@ class AnalysisService:
|
||||||
async def _run_analysis(self, analysis_id: str):
|
async def _run_analysis(self, analysis_id: str):
|
||||||
"""백그라운드에서 실제 분석을 실행하는 메서드"""
|
"""백그라운드에서 실제 분석을 실행하는 메서드"""
|
||||||
try:
|
try:
|
||||||
# 분석 상태를 RUNNING으로 변경
|
analysis = AnalysisVO(
|
||||||
analysis = self.analysis_repo.find_by_id(analysis_id)
|
id=analysis_id,
|
||||||
if analysis:
|
status=AnalysisStatus.RUNNING,
|
||||||
analysis.status = "running"
|
updated_at=datetime.now()
|
||||||
analysis.updated_at = datetime.now()
|
)
|
||||||
self.analysis_repo.update(analysis)
|
|
||||||
self.session.commit()
|
|
||||||
|
|
||||||
# 분석 정보 조회
|
analysis = self.analysis_repo.update(analysis)
|
||||||
analysis = self.analysis_repo.find_by_id(analysis_id)
|
|
||||||
if not analysis:
|
if not analysis:
|
||||||
return
|
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",
|
||||||
|
data={"status": "running", "message": "Analysis started"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# TradingAgentsGraph 설정 및 실행
|
# TradingAgentsGraph 설정 및 실행
|
||||||
config = self._create_config(analysis)
|
if analysis:
|
||||||
|
config = self._create_config(analysis)
|
||||||
|
|
||||||
# 분석 실행 (실제 구현)
|
# 분석 실행 (실제 구현)
|
||||||
await self._execute_trading_analysis(analysis_id, analysis, config)
|
await self._execute_trading_analysis(analysis_id, analysis, config)
|
||||||
|
|
||||||
# 분석 완료 상태로 변경
|
# 완료 상태로 업데이트
|
||||||
analysis = self.analysis_repo.find_by_id(analysis_id)
|
completed_analysis = AnalysisVO(
|
||||||
if analysis:
|
id=analysis_id,
|
||||||
analysis.status = "completed"
|
status=AnalysisStatus.COMPLETED,
|
||||||
analysis.completed_at = datetime.now()
|
completed_at=datetime.now(),
|
||||||
analysis.updated_at = datetime.now()
|
updated_at=datetime.now()
|
||||||
self.analysis_repo.update(analysis)
|
)
|
||||||
self.session.commit()
|
self.analysis_repo.update(completed_analysis)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 에러 발생 시 실패 상태로 변경
|
now = datetime.now()
|
||||||
analysis = self.analysis_repo.find_by_id(analysis_id)
|
updates = AnalysisVO(
|
||||||
if analysis:
|
status=AnalysisStatus.FAILED,
|
||||||
analysis.status = "failed"
|
error_message=str(e),
|
||||||
analysis.error_message = str(e)
|
completed_at = now,
|
||||||
analysis.completed_at = datetime.now()
|
updated_at = now
|
||||||
analysis.updated_at = datetime.now()
|
)
|
||||||
self.analysis_repo.update(analysis)
|
|
||||||
self.session.commit()
|
self.analysis_repo.update(updates)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
|
||||||
def _create_config(self, analysis: AnalysisVO) -> dict:
|
def _create_config(self, analysis: AnalysisVO) -> dict:
|
||||||
"""분석 설정을 생성하는 메서드"""
|
"""분석 설정을 생성하는 메서드"""
|
||||||
config = DEFAULT_CONFIG.copy() if DEFAULT_CONFIG else {}
|
config = {}
|
||||||
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,
|
||||||
|
|
@ -138,12 +172,17 @@ 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"Analysts selected: {analysis.analysts_selected}")
|
||||||
|
logger.info(f"Config: {config}")
|
||||||
|
|
||||||
# TradingAgentsGraph 초기화
|
# TradingAgentsGraph 초기화
|
||||||
graph = TradingAgentsGraph(
|
graph = TradingAgentsGraph(
|
||||||
analysis.analysts_selected,
|
analysis.analysts_selected,
|
||||||
config=config,
|
config=config,
|
||||||
debug=True
|
debug=True
|
||||||
)
|
)
|
||||||
|
logger.info("TradingAgentsGraph initialized successfully")
|
||||||
|
|
||||||
# 초기 상태 생성
|
# 초기 상태 생성
|
||||||
init_agent_state = graph.propagator.create_initial_state(
|
init_agent_state = graph.propagator.create_initial_state(
|
||||||
|
|
@ -153,8 +192,12 @@ class AnalysisService:
|
||||||
args = graph.propagator.get_graph_args()
|
args = graph.propagator.get_graph_args()
|
||||||
|
|
||||||
# 분석 실행 및 결과 처리
|
# 분석 실행 및 결과 처리
|
||||||
|
logger.info("Starting graph execution...")
|
||||||
trace = []
|
trace = []
|
||||||
|
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
|
||||||
|
logger.info(f"Processing chunk {chunk_count}: {list(chunk.keys()) if chunk else 'Empty chunk'}")
|
||||||
trace.append(chunk)
|
trace.append(chunk)
|
||||||
|
|
||||||
# 실시간으로 분석 결과 업데이트
|
# 실시간으로 분석 결과 업데이트
|
||||||
|
|
@ -167,12 +210,17 @@ class AnalysisService:
|
||||||
|
|
||||||
# 최종 보고서 생성
|
# 최종 보고서 생성
|
||||||
final_report = self._generate_final_report(final_state)
|
final_report = self._generate_final_report(final_state)
|
||||||
|
analysis.final_trade_decision = final_decision
|
||||||
|
analysis.final_report = final_report
|
||||||
|
|
||||||
# 최종 결과 저장
|
# 최종 결과 저장
|
||||||
self.analysis_repo.update(analysis_id, {
|
updates = AnalysisVO(
|
||||||
"final_trade_decision": final_decision,
|
id=analysis_id,
|
||||||
"final_report": final_report
|
final_trade_decision=final_decision,
|
||||||
})
|
final_report=final_report
|
||||||
|
)
|
||||||
|
self.analysis_repo.update(updates)
|
||||||
|
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -207,7 +255,10 @@ class AnalysisService:
|
||||||
|
|
||||||
# 업데이트가 있는 경우 저장
|
# 업데이트가 있는 경우 저장
|
||||||
if updates:
|
if updates:
|
||||||
self.analysis_repo.update(analysis_id, updates)
|
# analysis_id를 포함한 AnalysisVO 객체 생성
|
||||||
|
updates["id"] = analysis_id
|
||||||
|
updates_vo = AnalysisVO(**updates)
|
||||||
|
self.analysis_repo.update(updates_vo)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
def _generate_final_report(self, final_state: dict) -> str:
|
def _generate_final_report(self, final_state: dict) -> str:
|
||||||
|
|
@ -245,3 +296,4 @@ class AnalysisService:
|
||||||
report_parts.append(f"{final_state['risk_debate_state']['judge_decision']}")
|
report_parts.append(f"{final_state['risk_debate_state']['judge_decision']}")
|
||||||
|
|
||||||
return "\n\n".join(report_parts) if report_parts else "No analysis results available."
|
return "\n\n".join(report_parts) if report_parts else "No analysis results available."
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
from typing import Dict, Set
|
||||||
|
from fastapi import WebSocket
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketManager:
|
||||||
|
def __init__(self):
|
||||||
|
# Store active connections by member_id
|
||||||
|
self.active_connections: Dict[str, Set[WebSocket]] = {}
|
||||||
|
# Store analysis_id to member_id mapping
|
||||||
|
self.analysis_member_map: Dict[str, str] = {}
|
||||||
|
|
||||||
|
async def connect(self, websocket: WebSocket, member_id: str):
|
||||||
|
await websocket.accept()
|
||||||
|
if member_id not in self.active_connections:
|
||||||
|
self.active_connections[member_id] = set()
|
||||||
|
self.active_connections[member_id].add(websocket)
|
||||||
|
|
||||||
|
def disconnect(self, websocket: WebSocket, member_id: str):
|
||||||
|
if member_id in self.active_connections:
|
||||||
|
self.active_connections[member_id].discard(websocket)
|
||||||
|
if not self.active_connections[member_id]:
|
||||||
|
del self.active_connections[member_id]
|
||||||
|
|
||||||
|
def register_analysis(self, analysis_id: str, member_id: str):
|
||||||
|
"""Register which member owns which analysis"""
|
||||||
|
self.analysis_member_map[analysis_id] = member_id
|
||||||
|
|
||||||
|
async def send_analysis_update(self, analysis_id: str, update_type: str, data: dict):
|
||||||
|
"""Send analysis update to the member who owns the analysis"""
|
||||||
|
member_id = self.analysis_member_map.get(analysis_id)
|
||||||
|
if not member_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
message = {
|
||||||
|
"type": "analysis_update",
|
||||||
|
"analysis_id": analysis_id,
|
||||||
|
"update_type": update_type,
|
||||||
|
"data": data,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
await self.send_to_member(member_id, message)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def send_to_member(self, member_id: str, message: dict|str):
|
||||||
|
"""Send message to all connections of a specific member"""
|
||||||
|
if member_id not in self.active_connections:
|
||||||
|
return
|
||||||
|
|
||||||
|
dead_connections = set()
|
||||||
|
for connection in self.active_connections[member_id]:
|
||||||
|
try:
|
||||||
|
if isinstance(message, dict):
|
||||||
|
await connection.send_json(message)
|
||||||
|
else:
|
||||||
|
await connection.send_text(message)
|
||||||
|
except Exception:
|
||||||
|
dead_connections.add(connection)
|
||||||
|
|
||||||
|
# Clean up dead connections
|
||||||
|
for connection in dead_connections:
|
||||||
|
self.disconnect(connection, member_id)
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, field_validator
|
||||||
from datetime import datetime
|
from datetime import datetime, date
|
||||||
from typing import List, Dict
|
from typing import List, Dict, Union
|
||||||
|
from analysis.infra.db_models.analysis import AnalysisStatus
|
||||||
|
|
||||||
class Analysis(BaseModel):
|
class Analysis(BaseModel):
|
||||||
id: str | None = None
|
id: str
|
||||||
member_id: str
|
member_id: str | None = None
|
||||||
ticker: str
|
ticker: str | None = None
|
||||||
analysis_date: str
|
analysis_date: date | None = None
|
||||||
analysts_selected: List[str] = []
|
analysts_selected: list[str] = []
|
||||||
research_depth: int = 3
|
research_depth: int = 3
|
||||||
llm_provider: str = "openai"
|
llm_provider: str = "openai"
|
||||||
backend_url: str = "https://api.openai.com/v1"
|
backend_url: str = "https://api.openai.com/v1"
|
||||||
shallow_thinker: str = "gpt-4o-mini"
|
shallow_thinker: str = "gpt-4o"
|
||||||
deep_thinker: str = "gpt-4o"
|
deep_thinker: str = "o3"
|
||||||
status: str
|
status: AnalysisStatus = AnalysisStatus.PENDING
|
||||||
|
|
||||||
# 개별 분석가 리포트들
|
# 개별 분석가 리포트들
|
||||||
market_report: str | None = None
|
market_report: str | None = None
|
||||||
|
|
@ -33,5 +34,5 @@ class Analysis(BaseModel):
|
||||||
# 실행 결과 정보
|
# 실행 결과 정보
|
||||||
error_message: str | None = None
|
error_message: str | None = None
|
||||||
completed_at: datetime | None = None
|
completed_at: datetime | None = None
|
||||||
created_at: datetime
|
created_at: datetime | None = None
|
||||||
updated_at: datetime
|
updated_at: datetime | None = None
|
||||||
|
|
@ -2,7 +2,7 @@ from datetime import datetime,date
|
||||||
from typing import TYPE_CHECKING
|
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
|
from sqlalchemy import Column, Text
|
||||||
|
|
||||||
# TYPE_CHECKING을 사용해서 circular import 방지
|
# TYPE_CHECKING을 사용해서 circular import 방지
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -32,22 +32,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, description="Market Analyst 리포트")
|
market_report: str | None = Field(default=None, sa_column=Column(Text), description="Market Analyst 리포트")
|
||||||
sentiment_report: str | None = Field(default=None, description="Social Analyst 리포트")
|
sentiment_report: str | None = Field(default=None, sa_column=Column(Text), description="Social Analyst 리포트")
|
||||||
news_report: str | None = Field(default=None, description="News Analyst 리포트")
|
news_report: str | None = Field(default=None, sa_column=Column(Text), description="News Analyst 리포트")
|
||||||
fundamentals_report: str | None = Field(default=None, description="Fundamentals Analyst 리포트")
|
fundamentals_report: str | None = Field(default=None, sa_column=Column(Text), 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, description="Trading Team 계획")
|
trader_investment_plan: str | None = Field(default=None, sa_column=Column(Text), 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, description="최종 거래 결정")
|
final_trade_decision: str | None = Field(default=None, sa_column=Column(Text), description="최종 거래 결정")
|
||||||
final_report: str | None = Field(default=None, description="전체 통합 리포트")
|
final_report: str | None = Field(default=None, sa_column=Column(Text), description="전체 통합 리포트")
|
||||||
|
|
||||||
# 실행 결과 정보
|
# 실행 결과 정보
|
||||||
error_message: str | None = None
|
error_message: str | None = Field(default=None, sa_column=Column(Text))
|
||||||
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)
|
||||||
|
|
|
||||||
|
|
@ -28,30 +28,7 @@ class AnalysisRepository(IAnalysisRepository):
|
||||||
|
|
||||||
def save(self, analysis: AnalysisVO) -> AnalysisVO:
|
def save(self, analysis: AnalysisVO) -> AnalysisVO:
|
||||||
new_analysis = Analysis(
|
new_analysis = Analysis(
|
||||||
id=analysis.id,
|
**analysis.model_dump()
|
||||||
member_id=analysis.member_id,
|
|
||||||
ticker=analysis.ticker,
|
|
||||||
analysis_date=date.fromisoformat(analysis.analysis_date),
|
|
||||||
analysts_selected=analysis.analysts_selected,
|
|
||||||
research_depth=analysis.research_depth,
|
|
||||||
llm_provider=analysis.llm_provider,
|
|
||||||
backend_url=analysis.backend_url,
|
|
||||||
shallow_thinker=analysis.shallow_thinker,
|
|
||||||
deep_thinker=analysis.deep_thinker,
|
|
||||||
status=analysis.status,
|
|
||||||
market_report=analysis.market_report,
|
|
||||||
sentiment_report=analysis.sentiment_report,
|
|
||||||
news_report=analysis.news_report,
|
|
||||||
fundamentals_report=analysis.fundamentals_report,
|
|
||||||
investment_debate_state=analysis.investment_debate_state,
|
|
||||||
trader_investment_plan=analysis.trader_investment_plan,
|
|
||||||
risk_debate_state=analysis.risk_debate_state,
|
|
||||||
final_trade_decision=analysis.final_trade_decision,
|
|
||||||
final_report=analysis.final_report,
|
|
||||||
error_message=analysis.error_message,
|
|
||||||
completed_at=analysis.completed_at,
|
|
||||||
created_at=analysis.created_at,
|
|
||||||
updated_at=analysis.updated_at
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.session.add(new_analysis)
|
self.session.add(new_analysis)
|
||||||
|
|
@ -67,14 +44,12 @@ class AnalysisRepository(IAnalysisRepository):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# AnalysisVO의 데이터를 SQLModel 객체에 업데이트
|
# AnalysisVO의 데이터를 SQLModel 객체에 업데이트
|
||||||
vo_data = analysis_vo.sqlmodel_dump(exclude_unset=True)
|
analysis_data = analysis_vo.model_dump(exclude_unset=True)
|
||||||
for key, value in vo_data.items():
|
|
||||||
if hasattr(analysis, key) and key != 'id': # id는 변경하지 않음
|
|
||||||
setattr(analysis, key, value)
|
|
||||||
|
|
||||||
analysis.updated_at = datetime.now()
|
analysis.updated_at = datetime.now()
|
||||||
self.session.add(analysis)
|
analysis.sqlmodel_update(analysis_data)
|
||||||
|
|
||||||
self.session.flush()
|
self.session.flush()
|
||||||
self.session.refresh(analysis)
|
|
||||||
|
|
||||||
return AnalysisVO(**row_to_dict(analysis))
|
return AnalysisVO(**row_to_dict(analysis))
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from fastapi import APIRouter, Depends, BackgroundTasks, HTTPException, status
|
from fastapi import APIRouter, Depends, BackgroundTasks, HTTPException, status, WebSocket, WebSocketDisconnect
|
||||||
from analysis.interface.dto import (
|
from analysis.interface.dto import (
|
||||||
AnalysisSessionResponse,
|
AnalysisSessionResponse,
|
||||||
TradingAnalysisRequest,
|
TradingAnalysisRequest,
|
||||||
|
|
@ -9,6 +9,7 @@ from utils.auth import get_current_member, CurrentMember
|
||||||
from dependency_injector.wiring import inject, Provide
|
from dependency_injector.wiring import inject, Provide
|
||||||
from analysis.application.analysis_service import AnalysisService
|
from analysis.application.analysis_service import AnalysisService
|
||||||
from utils.containers import Container
|
from utils.containers import Container
|
||||||
|
from analysis.application.websocket_manager import WebSocketManager
|
||||||
|
|
||||||
router = APIRouter(prefix="/analysis", tags=["analysis"])
|
router = APIRouter(prefix="/analysis", tags=["analysis"])
|
||||||
|
|
||||||
|
|
@ -40,6 +41,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)
|
||||||
|
|
@ -69,7 +71,7 @@ def get_analysis_result(
|
||||||
return AnalysisResultResponse(
|
return AnalysisResultResponse(
|
||||||
id=analysis.id,
|
id=analysis.id,
|
||||||
ticker=analysis.ticker,
|
ticker=analysis.ticker,
|
||||||
analysis_date=analysis.analysis_date,
|
analysis_date=analysis.analysis_date.isoformat() if hasattr(analysis.analysis_date, 'isoformat') else str(analysis.analysis_date),
|
||||||
status=analysis.status,
|
status=analysis.status,
|
||||||
market_report=analysis.market_report,
|
market_report=analysis.market_report,
|
||||||
sentiment_report=analysis.sentiment_report,
|
sentiment_report=analysis.sentiment_report,
|
||||||
|
|
@ -106,3 +108,30 @@ def get_analysis_status(
|
||||||
"updated_at": analysis.updated_at.isoformat(),
|
"updated_at": analysis.updated_at.isoformat(),
|
||||||
"error_message": analysis.error_message
|
"error_message": analysis.error_message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@router.websocket("/ws")
|
||||||
|
@inject
|
||||||
|
async def websocket_endpoint(
|
||||||
|
websocket: WebSocket,
|
||||||
|
current_member: Annotated[CurrentMember, Depends(get_current_member)],
|
||||||
|
websocket_manager: Annotated[WebSocketManager, Depends(Provide[Container.websocket_manager])]
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
WebSocket endpoint for real-time analysis updates
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Connect the websocket
|
||||||
|
await websocket_manager.connect(websocket, current_member.id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Keep connection alive
|
||||||
|
while True:
|
||||||
|
# Wait for messages from client (like ping/pong)
|
||||||
|
data = await websocket.receive_text()
|
||||||
|
# Echo back for heartbeat
|
||||||
|
if data == "ping":
|
||||||
|
await websocket.send_text("pong")
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
websocket_manager.disconnect(websocket, current_member.id)
|
||||||
|
except Exception as e:
|
||||||
|
await websocket.close(code=1011, reason=str(e))
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,14 @@ class AnalystType(str, Enum):
|
||||||
FUNDAMENTALS = "fundamentals"
|
FUNDAMENTALS = "fundamentals"
|
||||||
|
|
||||||
class TradingAnalysisRequest(BaseModel):
|
class TradingAnalysisRequest(BaseModel):
|
||||||
ticker: str
|
ticker: str = "NVDA"
|
||||||
analysis_date: str
|
analysis_date: str = "2025-07-07"
|
||||||
analysts: List[AnalystType]
|
analysts: List[AnalystType] = [AnalystType.MARKET, AnalystType.SOCIAL, AnalystType.NEWS, AnalystType.FUNDAMENTALS]
|
||||||
research_depth: int = 3
|
research_depth: int = 3
|
||||||
llm_provider: str = "openai"
|
llm_provider: str = "openai"
|
||||||
backend_url: str = "https://api.openai.com/v1"
|
backend_url: str = "https://api.openai.com/v1"
|
||||||
shallow_thinker: str = "gpt-4o-mini"
|
shallow_thinker: str = "gpt-4o-mini"
|
||||||
deep_thinker: str = "gpt-4o"
|
deep_thinker: str = "gpt-4o-mini"
|
||||||
|
|
||||||
class AnalysisSessionResponse(BaseModel):
|
class AnalysisSessionResponse(BaseModel):
|
||||||
id : str
|
id : str
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,16 @@ 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
|
||||||
|
|
||||||
|
# 로깅 설정
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.StreamHandler(), # 콘솔 출력
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,12 @@ class MemberService:
|
||||||
self,
|
self,
|
||||||
member_repo: IMemberRepository,
|
member_repo: IMemberRepository,
|
||||||
crypto: Crypto,
|
crypto: Crypto,
|
||||||
db_session: Session,
|
session: Session,
|
||||||
ulid: ULID
|
ulid: ULID
|
||||||
):
|
):
|
||||||
self.member_repo = member_repo
|
self.member_repo = member_repo
|
||||||
self.crypto = crypto
|
self.crypto = crypto
|
||||||
self.db_session = db_session
|
self.db_session = session
|
||||||
self.ulid = ulid
|
self.ulid = ulid
|
||||||
|
|
||||||
def create_member(
|
def create_member(
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ from utils.containers import Container
|
||||||
from dependency_injector.wiring import inject, Provide
|
from dependency_injector.wiring import inject, Provide
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
from utils.auth import get_current_member, CurrentMember, get_admin_member
|
from utils.auth import get_current_member, CurrentMember, get_admin_member
|
||||||
|
from analysis.interface.dto import AnalysisSessionResponse
|
||||||
|
from analysis.application.analysis_service import AnalysisService
|
||||||
|
|
||||||
router = APIRouter(prefix="/members", tags=["members"])
|
router = APIRouter(prefix="/members", tags=["members"])
|
||||||
|
|
||||||
|
|
@ -67,12 +69,3 @@ def get_member(
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Member not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Member not found")
|
||||||
return member
|
return member
|
||||||
|
|
||||||
# @router.get("/analysis-sessions", response_model=list[AnalysisSessionResponse])
|
|
||||||
# @inject
|
|
||||||
# def get_member_analysis_sessions(
|
|
||||||
# current_member: Annotated[CurrentMember | None, Depends(get_current_member)] = None,
|
|
||||||
# member_service: Annotated[MemberService | None, Depends(Provide[Container.member_service])] = None
|
|
||||||
# ):
|
|
||||||
|
|
||||||
# result = member_service.get_analysis_sessions_by_member(current_member.id)
|
|
||||||
# return result
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ from member.infra.repository.member_repo import MemberRepository
|
||||||
from member.application.member_service import MemberService
|
from member.application.member_service import MemberService
|
||||||
from analysis.application.analysis_service import AnalysisService
|
from analysis.application.analysis_service import AnalysisService
|
||||||
from analysis.infra.repository.analysis_repo import AnalysisRepository
|
from analysis.infra.repository.analysis_repo import AnalysisRepository
|
||||||
|
from analysis.application.websocket_manager import WebSocketManager
|
||||||
from ulid import ULID
|
from ulid import ULID
|
||||||
|
|
||||||
class Container(containers.DeclarativeContainer):
|
class Container(containers.DeclarativeContainer):
|
||||||
|
|
@ -12,32 +13,37 @@ class Container(containers.DeclarativeContainer):
|
||||||
packages=["member", "analysis"]
|
packages=["member", "analysis"]
|
||||||
)
|
)
|
||||||
|
|
||||||
db_session = providers.Resource(get_session)
|
session = providers.Resource(get_session)
|
||||||
crypto = providers.Factory(Crypto)
|
crypto = providers.Factory(Crypto)
|
||||||
ulid = providers.Factory(ULID)
|
ulid = providers.Factory(ULID)
|
||||||
|
|
||||||
member_repo = providers.Factory(
|
member_repo = providers.Factory(
|
||||||
MemberRepository,
|
MemberRepository,
|
||||||
session=db_session
|
session=session
|
||||||
)
|
)
|
||||||
|
|
||||||
member_service = providers.Factory(
|
member_service = providers.Factory(
|
||||||
MemberService,
|
MemberService,
|
||||||
member_repo=member_repo,
|
member_repo=member_repo,
|
||||||
crypto=crypto,
|
crypto=crypto,
|
||||||
db_session=db_session,
|
session=session,
|
||||||
ulid=ulid
|
ulid=ulid
|
||||||
)
|
)
|
||||||
|
|
||||||
analysis_repo = providers.Factory(
|
analysis_repo = providers.Factory(
|
||||||
AnalysisRepository,
|
AnalysisRepository,
|
||||||
session=db_session
|
session=session
|
||||||
|
)
|
||||||
|
|
||||||
|
websocket_manager = providers.Singleton(
|
||||||
|
WebSocketManager
|
||||||
)
|
)
|
||||||
|
|
||||||
analysis_service = providers.Factory(
|
analysis_service = providers.Factory(
|
||||||
AnalysisService,
|
AnalysisService,
|
||||||
analysis_repo=analysis_repo,
|
analysis_repo=analysis_repo,
|
||||||
db_session=db_session,
|
session=session,
|
||||||
ulid=ulid
|
ulid=ulid,
|
||||||
|
websocket_manager=websocket_manager
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue