[add] ai 분석 됨. 프론트엔드만 다듬자

This commit is contained in:
kimheesu 2025-06-13 16:01:31 +09:00
parent 2c77fcef1f
commit 42371adff9
12 changed files with 4075 additions and 135 deletions

File diff suppressed because it is too large Load Diff

View File

@ -119,3 +119,47 @@ Adhere strictly to these instructions, and ensure your output is detailed, accur
"RISK JUDGE", judge_decision, situation, returns_losses
)
risk_manager_memory.add_situations([(situation, result)])
@staticmethod
def generate_final_report(final_state: dict) -> str:
"""Generate a final, comprehensive report from the final state."""
report_parts = []
report_parts.append(f"# 최종 분석 보고서: {final_state.get('company_of_interest', 'N/A')}")
report_parts.append(f"**분석 기준일:** {final_state.get('trade_date', 'N/A')}")
report_parts.append("---")
# 각 분석가 리포트 추가
if final_state.get('market_report'):
report_parts.append("## 시장 분석가 리포트")
report_parts.append(final_state['market_report'])
if final_state.get('sentiment_report'):
report_parts.append("## 소셜 미디어 분석가 리포트")
report_parts.append(final_state['sentiment_report'])
if final_state.get('news_report'):
report_parts.append("## 뉴스 분석가 리포트")
report_parts.append(final_state['news_report'])
if final_state.get('fundamentals_report'):
report_parts.append("## 재무 분석가 리포트")
report_parts.append(final_state['fundamentals_report'])
# 투자 토론 요약 추가
if final_state.get('investment_debate_state'):
debate = final_state['investment_debate_state']
report_parts.append("## 투자 결정 토론 요약")
report_parts.append(f"**심사위원 최종 결정:** {debate.get('judge_decision', 'N/A')}")
# 최종 투자 계획 및 결정 추가
if final_state.get('investment_plan'):
report_parts.append("## 최종 투자 계획")
report_parts.append(final_state['investment_plan'])
if final_state.get('final_trade_decision'):
report_parts.append("## 최종 거래 결정")
report_parts.append(final_state['final_trade_decision'])
report = "\n\n".join(report_parts)
return report

View File

@ -1,6 +1,6 @@
# TradingAgents/graph/setup.py
from typing import Dict, Any
from typing import Dict, Any, List
from langchain_openai import ChatOpenAI
from langgraph.graph import END, StateGraph, START
from langgraph.prebuilt import ToolNode
@ -40,9 +40,7 @@ class GraphSetup:
self.risk_manager_memory = risk_manager_memory
self.conditional_logic = conditional_logic
def setup_graph(
self, selected_analysts=["market", "social", "news", "fundamentals"]
):
def setup_graph(self, selected_analysts: List[str]):
"""Set up and compile the agent workflow graph.
Args:

View File

@ -5,6 +5,7 @@ from pathlib import Path
import json
from datetime import date
from typing import Dict, Any, Tuple, List, Optional
import asyncio
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import ToolNode
@ -31,19 +32,20 @@ class TradingAgentsGraph:
def __init__(
self,
selected_analysts=["market", "social", "news", "fundamentals"],
debug=False,
config: Dict[str, Any] = None,
progress_callback=None,
debug=False,
):
"""Initialize the trading agents graph and components.
Args:
selected_analysts: List of analyst types to include
debug: Whether to run in debug mode
config: Configuration dictionary. If None, uses default config
progress_callback: Async function to send progress updates
debug: Whether to run in debug mode
"""
self.debug = debug
self.config = config or DEFAULT_CONFIG
self.progress_callback = progress_callback
# Update the interface's config
set_config(self.config)
@ -95,8 +97,9 @@ class TradingAgentsGraph:
self.ticker = None
self.log_states_dict = {} # date to full state dict
# Set up the graph
self.graph = self.graph_setup.setup_graph(selected_analysts)
# Set up the graph with default analysts initially
default_analysts = ["market", "social", "news", "fundamentals"]
self.graph = self.graph_setup.setup_graph(default_analysts)
def _create_tool_nodes(self) -> Dict[str, ToolNode]:
"""Create tool nodes for different data sources."""
@ -143,8 +146,55 @@ class TradingAgentsGraph:
),
}
def invoke(self, input_data: Dict) -> Dict:
"""Run the trading agents graph for a web-based request."""
self.ticker = input_data.get("ticker", "UNKNOWN")
trade_date = input_data.get("date", date.today().strftime("%Y-%m-%d"))
selected_analysts = input_data.get("selected_analysts", [])
self.graph = self.graph_setup.setup_graph(selected_analysts)
init_agent_state = self.propagator.create_initial_state(
self.ticker, trade_date
)
args = self.propagator.get_graph_args()
final_report = ""
final_state_result = None
# 진행률 계산을 위한 변수
total_steps = len(self.graph.nodes)
step_count = 0
# Stream the graph execution to get real-time updates
for chunk in self.graph.stream(init_agent_state, **args):
# 1 청크당 1단계로 간주
step_count += 1
for node_name, node_output in chunk.items():
if self.progress_callback:
agent_name = node_name.replace("_node", "").replace("_", " ").title()
message = f"Step {step_count}/{total_steps}: {agent_name} is working..."
# 계산된 진행률과 함께 콜백 호출
asyncio.run(self.progress_callback(
"agent_update",
message,
agent_name,
step=step_count,
total=total_steps
))
final_state_result = chunk
if final_state_result:
final_report = self.reflector.generate_final_report(final_state_result)
self._log_state(trade_date, final_state_result)
return {"final_report": final_report}
def propagate(self, company_name, trade_date):
"""Run the trading agents graph for a company on a specific date."""
"""Run the trading agents graph for a company on a specific date (CLI)."""
self.ticker = company_name

View File

@ -2,6 +2,7 @@ from rest_framework import serializers
from django.contrib.auth import authenticate
from django.contrib.auth.password_validation import validate_password
from .models import User, UserProfile, AnalysisSession
from datetime import date
class UserRegistrationSerializer(serializers.ModelSerializer):
@ -121,11 +122,17 @@ class CreateAnalysisSessionSerializer(serializers.ModelSerializer):
class Meta:
model = AnalysisSession
fields = (
'ticker', 'analysis_date',
'ticker',
'analysts_selected', 'research_depth',
'shallow_thinker', 'deep_thinker'
)
# analysis_date는 create 시점에 자동 생성되므로 필드에서 제외
def create(self, validated_data):
"""오늘 날짜를 추가하여 세션 생성"""
validated_data['analysis_date'] = date.today()
return super().create(validated_data)
def validate_analysts_selected(self, value):
"""선택된 분석가들 검증"""
if not isinstance(value, list) or len(value) == 0:

View File

@ -3,6 +3,7 @@ import datetime
from typing import Dict, List, Optional
from django.conf import settings
from channels.layers import get_channel_layer
from channels.db import database_sync_to_async
from asgiref.sync import async_to_sync
# CLI 모듈 import (경로 조정 필요)
@ -25,41 +26,50 @@ class TradingAnalysisService:
self.channel_layer = get_channel_layer()
self.user_channel_group = f"user_{user.id}"
@database_sync_to_async
def _update_session_status(self, status: str, error_message: Optional[str] = None, final_report: Optional[str] = None):
"""세션 상태 업데이트 (비동기 안전)"""
self.session.status = status
now = datetime.datetime.now()
if status == 'running':
self.session.started_at = now
else:
self.session.completed_at = now
if error_message:
self.session.error_message = error_message
if final_report:
self.session.final_report = final_report
self.session.save()
@database_sync_to_async
def _get_user_profile_and_key(self):
"""사용자 프로필 및 API 키 조회 (비동기 안전)"""
profile = self.user.profile
return profile.get_effective_openai_api_key()
async def run_analysis(self):
"""분석 실행"""
try:
# 세션 상태 업데이트
self.session.status = 'running'
self.session.started_at = datetime.datetime.now()
self.session.save()
await self._update_session_status('running')
# WebSocket으로 시작 알림
await self._send_websocket_message({
'type': 'analysis_started',
'session_id': self.session.id,
'message': '분석을 시작합니다...'
})
# 사용자 프로필에서 OpenAI API 키 가져오기
profile = self.user.profile
api_key = profile.get_effective_openai_api_key()
api_key = await self._get_user_profile_and_key()
if not api_key:
raise Exception("OpenAI API 키가 설정되지 않았습니다.")
# CLI 설정 준비
config = self._prepare_analysis_config(api_key)
# 분석 실행
result = await self._execute_trading_analysis(config)
# 결과 저장
self.session.final_report = result
self.session.status = 'completed'
self.session.completed_at = datetime.datetime.now()
self.session.save()
await self._update_session_status('completed', final_report=result)
# WebSocket으로 완료 알림
await self._send_websocket_message({
'type': 'analysis_completed',
'session_id': self.session.id,
@ -70,17 +80,13 @@ class TradingAnalysisService:
return result
except Exception as e:
# 에러 처리
self.session.status = 'failed'
self.session.error_message = str(e)
self.session.completed_at = datetime.datetime.now()
self.session.save()
error_msg = str(e)
await self._update_session_status('failed', error_message=error_msg)
# WebSocket으로 에러 알림
await self._send_websocket_message({
'type': 'analysis_failed',
'session_id': self.session.id,
'message': f'분석 중 오류가 발생했습니다: {str(e)}'
'message': f'분석 중 오류가 발생했습니다: {error_msg}'
})
raise e
@ -115,9 +121,28 @@ class TradingAnalysisService:
'deep_thinking_model': config['deep_thinker'],
})
# TradingAgentsGraph 초기화
trading_graph = TradingAgentsGraph(analysis_config)
# 진행 상황 콜백 함수 수정
async def progress_callback(message_type: str, content: str, agent: str = None, step: int = 0, total: int = 0):
# 백엔드에서 진행률 계산
progress_percent = int((step / total) * 99) if total > 0 else 0 # 100%는 완료 시에만
await self._send_websocket_message({
'type': 'analysis_progress',
'session_id': self.session.id,
'message_type': message_type,
'content': content,
'agent': agent,
'progress': progress_percent,
})
# TradingAgentsGraph 초기화 (더 상세한 예외 처리)
try:
trading_graph = TradingAgentsGraph(
config=analysis_config,
progress_callback=progress_callback
)
except Exception as e:
raise Exception(f"TradingAgentsGraph 초기화 실패: {str(e)}")
# 분석 입력 데이터 준비
input_data = {
'ticker': config['ticker'],
@ -126,50 +151,22 @@ class TradingAnalysisService:
'research_depth': config['research_depth'],
}
# 진행 상황 콜백 함수
async def progress_callback(message_type: str, content: str, agent: str = None):
await self._send_websocket_message({
'type': 'analysis_progress',
'session_id': self.session.id,
'message_type': message_type,
'content': content,
'agent': agent
})
# 분석 실행 (실제 CLI 로직 호출)
# 여기서는 간단화된 버전으로 구현
# 실제로는 trading_graph.invoke(input_data) 형태로 호출
# 분석 진행 상황 시뮬레이션
analysis_steps = [
("Market Analyst", "시장 데이터 분석 중..."),
("Social Analyst", "소셜 센티멘트 분석 중..."),
("News Analyst", "뉴스 분석 중..."),
("Fundamentals Analyst", "기본 분석 중..."),
("Research Manager", "연구 결과 종합 중..."),
("Trader", "거래 전략 수립 중..."),
("Portfolio Manager", "포트폴리오 최적화 중...")
]
final_report_parts = []
for agent, message in analysis_steps:
await progress_callback("agent_update", message, agent)
# 실제 분석 로직 호출 (여기서는 시뮬레이션)
await asyncio.sleep(2) # 실제 분석 시간 시뮬레이션
# 각 단계별 결과 생성 (실제로는 trading_graph의 결과)
step_result = f"## {agent} 분석 결과\n\n{config['ticker']} 종목에 대한 {agent.lower()} 분석을 완료했습니다.\n"
final_report_parts.append(step_result)
# 최종 보고서 생성
final_report = "\n\n".join(final_report_parts)
return final_report
try:
# 여기서 trading_graph.invoke를 비동기로 실행해야 합니다.
# 현재 trading_graph.invoke가 동기 함수라고 가정하고,
# asyncio.to_thread를 사용해 비동기 컨텍스트에서 실행합니다.
result = await asyncio.to_thread(
trading_graph.invoke,
input_data
)
return result['final_report'] # 결과 형식에 따라 조정 필요
except Exception as e:
raise Exception(f"trading_graph.invoke 실행 실패: {str(e)}")
except Exception as e:
raise Exception(f"분석 실행 중 오류 발생: {str(e)}")
# 에러 메시지를 명확하게 다시 던짐
raise Exception(f"분석 실행 중 오류: {str(e)}")
async def _send_websocket_message(self, message: Dict):
"""WebSocket으로 메시지 전송"""
@ -189,24 +186,15 @@ class TradingAnalysisManager:
"""거래 분석 관리자"""
@staticmethod
def create_analysis_session(user, analysis_data: Dict) -> AnalysisSession:
"""분석 세션 생성"""
session = AnalysisSession.objects.create(
user=user,
ticker=analysis_data['ticker'],
analysis_date=analysis_data['analysis_date'],
analysts_selected=analysis_data['analysts_selected'],
research_depth=analysis_data['research_depth'],
shallow_thinker=analysis_data['shallow_thinker'],
deep_thinker=analysis_data['deep_thinker'],
)
return session
@database_sync_to_async
def _get_session(user, session_id):
return AnalysisSession.objects.get(id=session_id, user=user)
@staticmethod
async def start_analysis(user, session_id: int):
"""분석 시작"""
try:
session = AnalysisSession.objects.get(id=session_id, user=user)
session = await TradingAnalysisManager._get_session(user, session_id)
service = TradingAnalysisService(user, session)
result = await service.run_analysis()
return result
@ -214,11 +202,13 @@ class TradingAnalysisManager:
raise Exception("분석 세션을 찾을 수 없습니다.")
@staticmethod
@database_sync_to_async
def get_user_analysis_sessions(user) -> List[AnalysisSession]:
"""사용자의 분석 세션 목록 조회"""
return AnalysisSession.objects.filter(user=user).order_by('-created_at')
return list(AnalysisSession.objects.filter(user=user).order_by('-created_at'))
@staticmethod
@database_sync_to_async
def cancel_analysis(user, session_id: int):
"""분석 취소"""
try:

View File

@ -5,6 +5,7 @@ from rest_framework.views import APIView
from django.shortcuts import get_object_or_404
from asgiref.sync import sync_to_async
import asyncio
import threading
from datetime import datetime
from apps.authentication.models import AnalysisSession
@ -56,15 +57,15 @@ class StartAnalysisView(APIView):
def post(self, request):
"""새로운 분석 시작"""
print(f"request.data: {request.data}")
serializer = CreateAnalysisSessionSerializer(data=request.data)
if serializer.is_valid():
# 분석 세션 생성
session = serializer.save(user=request.user)
# 백그라운드에서 분석 실행
# 실제 환경에서는 Celery나 다른 task queue를 사용하는 것이 좋습니다
asyncio.create_task(self._start_analysis_async(request.user, session.id))
# 별도의 스레드에서 비동기 작업 실행
thread = threading.Thread(target=self.run_async_task, args=(self._start_analysis_async(request.user, session.id),))
thread.start()
return Response({
'message': '분석이 시작되었습니다.',
@ -74,6 +75,13 @@ class StartAnalysisView(APIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def run_async_task(self, coro):
"""새 이벤트 루프에서 비동기 코루틴 실행"""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(coro)
loop.close()
async def _start_analysis_async(self, user, session_id):
"""비동기 분석 실행"""
try:

File diff suppressed because one or more lines are too long

View File

@ -1,32 +1,32 @@
#!/bin/bash
# #!/bin/bash
echo "🚀 Django 서버 시작 - 데이터베이스 초기화"
# echo "🚀 Django 서버 시작 - 데이터베이스 초기화"
# Django 설정 모듈 환경 변수 설정
export DJANGO_SETTINGS_MODULE=tradingagents_web.settings
# # Django 설정 모듈 환경 변수 설정
# export DJANGO_SETTINGS_MODULE=tradingagents_web.settings
# 1. 데이터베이스 초기화
echo "🔄 데이터베이스 초기화 중..."
docker exec -i tradingagents_mysql mysql -u root -ppassword -e "
DROP DATABASE IF EXISTS tradingagents_db;
CREATE DATABASE tradingagents_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
"
# # 1. 데이터베이스 초기화
# echo "🔄 데이터베이스 초기화 중..."
# docker exec -i tradingagents_mysql mysql -u root -ppassword -e "
# DROP DATABASE IF EXISTS tradingagents_db;
# CREATE DATABASE tradingagents_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
# "
# 2. 마이그레이션
echo "🔄 마이그레이션 중..."
python manage.py makemigrations authentication
python manage.py makemigrations
python manage.py migrate
# # 2. 마이그레이션
# echo "🔄 마이그레이션 중..."
# python manage.py makemigrations authentication
# python manage.py makemigrations
# python manage.py migrate
# 3. 관리자 계정 생성
echo "🔄 관리자 계정 생성 중..."
python manage.py shell -c "
from django.contrib.auth import get_user_model;
User = get_user_model();
if not User.objects.filter(email='admin@example.com').exists():
User.objects.create_superuser('admin@example.com', 'admin', 'admin123!');
print('✅ 관리자: admin@example.com / admin123!');
"
# # 3. 관리자 계정 생성
# echo "🔄 관리자 계정 생성 중..."
# python manage.py shell -c "
# from django.contrib.auth import get_user_model;
# User = get_user_model();
# if not User.objects.filter(email='admin@example.com').exists():
# User.objects.create_superuser('admin@example.com', 'admin', 'admin123!');
# print('✅ 관리자: admin@example.com / admin123!');
# "
# 4. 서버 시작 (환경 변수와 함께)
echo "🎉 서버 시작!"

View File

@ -136,9 +136,10 @@ export const WebSocketProvider = ({ children }) => {
...prev,
[data.session_id]: {
...prev[data.session_id],
status: 'running',
message: data.content,
agent: data.agent,
progress: prev[data.session_id]?.progress + 10 || 10
progress: data.progress,
}
}));
@ -160,7 +161,7 @@ export const WebSocketProvider = ({ children }) => {
status: 'completed',
message: data.message,
progress: 100,
result: data.result
result: data.result,
}
}));
message.success(data.message);

View File

@ -1,7 +1,7 @@
// web/frontend/src/pages/Analysis/Analysis.js
import React, { useState, useEffect } from 'react';
import { Card, Divider, Spin, Alert, Typography } from 'antd';
import { Card, Divider, Spin, Alert, Typography, message } from 'antd';
import styled from 'styled-components';
import api from '../../services/api';
import { useWebSocket } from '../../contexts/WebSocketContext';
@ -54,7 +54,7 @@ const Analysis = () => {
if(currentSessionId) clearAnalysisProgress(currentSessionId);
try {
const response = await api.post('/api/trading/start-analysis/', values);
const response = await api.post('/api/trading/start/', values);
const { session_id } = response.data;
setCurrentSessionId(session_id);
@ -78,6 +78,20 @@ const Analysis = () => {
clearMessages();
};
// 분석 취소 핸들러
const handleCancelAnalysis = async () => {
if (!currentSessionId) return;
try {
await api.post(`/api/trading/cancel/${currentSessionId}/`);
message.success('분석이 성공적으로 중단되었습니다.');
handleNewAnalysis(); // 상태를 초기화하고 폼으로 돌아감
} catch (err) {
const errorMessage = err.response?.data?.error || '분석 취소에 실패했습니다.';
setError(errorMessage);
}
};
const renderContent = () => {
if (analysisStatus === 'starting') {
return <div style={{ textAlign: 'center', padding: '50px' }}><Spin size="large" tip="분석을 시작하고 있습니다..." /></div>;
@ -92,6 +106,7 @@ const Analysis = () => {
messages={messages.filter(m => m.sessionId === currentSessionId)}
finalReport={finalReport}
onNewAnalysis={handleNewAnalysis}
onCancelAnalysis={handleCancelAnalysis}
/>
);
}

View File

@ -1,3 +1,5 @@
// web/frontend/src/pages/Analysis/components/AnalysisForm.js
import React from 'react';
import { Form, Input, Button, Card, Select, Slider, Checkbox, Row, Col, Typography } from 'antd';
import { FundOutlined, SendOutlined } from '@ant-design/icons';
@ -18,6 +20,24 @@ const analystsOptions = [
{ label: '재무 분석가 (Fundamentals)', value: 'fundamentals' },
];
const shallowThinkerOptions = [
{ value: 'gpt-4o-mini', label: 'GPT-4o-mini - 빠르고 효율적인 모델' },
{ value: 'gpt-4.1-nano', label: 'GPT-4.1-nano - 초경량 모델' },
{ value: 'gpt-4.1-mini', label: 'GPT-4.1-mini - 준수한 성능의 컴팩트 모델' },
{ value: 'gpt-4o', label: 'GPT-4o - 표준 모델' },
];
const deepThinkerOptions = [
{ value: 'gpt-4.1-nano', label: 'GPT-4.1-nano - 초경량 모델' },
{ value: 'gpt-4.1-mini', label: 'GPT-4.1-mini - 준수한 성능의 컴팩트 모델' },
{ value: 'gpt-4o', label: 'GPT-4o - 표준 모델' },
{ value: 'o4-mini', label: 'o4-mini - 특화된 소형 추론 모델' },
{ value: 'o3-mini', label: 'o3-mini - 경량 고급 추론 모델' },
{ value: 'o3', label: 'o3 - 전체 고급 추론 모델' },
{ value: 'o1', label: 'o1 - 최상위 추론 및 문제 해결 모델' },
];
const AnalysisForm = ({ onStartAnalysis, loading }) => {
const [form] = Form.useForm();
@ -83,17 +103,22 @@ const AnalysisForm = ({ onStartAnalysis, loading }) => {
<Col span={12}>
<Form.Item name="shallow_thinker" label="Shallow Thinker 모델">
<Select>
<Option value="gpt-4o-mini">GPT-4o Mini</Option>
<Option value="gpt-4o">GPT-4o</Option>
<Option value="gpt-4-turbo">GPT-4 Turbo</Option>
{shallowThinkerOptions.map(option => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="deep_thinker" label="Deep Thinker 모델">
<Select>
<Option value="gpt-4o">GPT-4o</Option>
<Option value="gpt-4-turbo">GPT-4 Turbo</Option>
{deepThinkerOptions.map(option => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
))}
</Select>
</Form.Item>
</Col>
@ -116,4 +141,4 @@ const AnalysisForm = ({ onStartAnalysis, loading }) => {
);
};
export default AnalysisForm;
export default AnalysisForm;